How to Build a Wizard That Changes Its Route at Runtime

A wizard with a fixed list of pages is simple to build. The work starts when a choice on one page decides which pages follow, when Back must return to a page with its input intact, and when a failed Finish has to leave the user in the wizard to try again.

TWizard<T> in Sphere 10 Pascal Framework handles that navigation. Each screen is a form that can validate itself and rewrite the remaining path, and the wizard keeps every screen instance alive for the whole run. This article covers how screens and the wizard are declared, how a screen changes the route with UpdatePath, and how FinishRequested makes completion retryable.

A multi-step wizard with alternative routes through its screens.
A multi-step wizard with alternative routes through its screens.
💡 In short. Build a TWizard whose screens validate input, change the remaining route with UpdatePath, keep their state when removed from the path and allow Finish to be retried.

1. A first look

A screen chooses the pages that follow it from its OnNext hook by calling UpdatePath. The sample's first screen reads a flag on the wizard's model and continues either through a detail screen to a summary or straight to the summary.

Pascal
procedure TChoiceScreen.OnNext;
begin
  inherited;
  if Model.UseDetail then
    UpdatePath(ptReplaceAllNext, [TDetailScreen, TSummaryScreen])
  else
    UpdatePath(ptReplaceAllNext, [TSummaryScreen]);
end;

ptReplaceAllNext replaces everything after the current screen with the listed screen classes, so the route is decided when the user presses Next. The rest of the article explains the parts this method relies on.

2. How the wizard is put together

A wizard has three parts. The model is a value of type T that every screen reads and writes, and it stays owned by the caller. The screens are forms derived from TWizardForm<T>, which the wizard creates and owns. The path is the ordered list of screen classes that the user will see, and it can change while the wizard runs.

Pressing Next always runs the same sequence on the current screen.

If Validate returns False, the sequence stops there and the screen stays active.

3. Declare the screens and the wizard

A screen overrides only the hooks it needs. A wizard class must implement FinishRequested and CancelRequested.

Pascal
  TChoiceScreen = class(TWizardForm<TOptions>)
  public
    constructor Create(AOwner: TComponent); override;
    procedure OnNext; override;
  end;
  TDetailScreen = class(TWizardForm<TOptions>)
  public
    Draft: String;
    constructor Create(AOwner: TComponent); override;
    function Validate(out AMessage: String): Boolean; override;
    procedure OnNext; override;
  end;

TOptions is the sample's model class. It holds a flag that selects the route, the value collected by the detail screen and a flag that lets the sample reject the first Finish.

Screen hook When it runs
Initialize Once, after the model has been assigned, including for screens added later.
OnPresent Each time the screen is shown.
Validate On Next, before anything else. Returning False keeps the screen active.
OnNext After a successful Validate. This is where a screen commits its input to the model and may change the path.
OnPrevious On Back.

The wizard is created with its starting path and started with the model. Start initializes the screens and shows the host form modally.

Pascal
    Wizard := TSampleWizard.Create(nil, [TChoiceScreen]);
    try
      Wizard.Start(Options);

TActionWizard<T> provides the same behaviour without a subclass by taking the finish and cancel handlers as method references.

4. Let a screen choose the remaining route

The starting path above contains a single screen. That screen declares the wstAssumeHasNext trait, which tells the wizard that a successor will be supplied from OnNext and that the button should read Next rather than Finish.

Pascal
constructor TChoiceScreen.Create(AOwner: TComponent);
begin
  inherited CreateNew(AOwner);
  Traits := [wstAssumeHasNext];
end;

The successor comes from the TChoiceScreen.OnNext method shown in the first look. UpdatePath takes an update type and a list of screen classes.

Update type Effect on the path
ptInject Inserts the screens directly after the current one.
ptReplaceAllNext Replaces everything after the current screen.
ptReplaceAll Replaces the whole path.
ptRemoveNext Removes the listed screens from the part of the path that follows.

Repeating an identical update is harmless, so OnNext can call it on every visit. Going Back restores the path as it was when the earlier screen was presented.

A screen that leaves the path is not destroyed. The wizard keeps one instance per screen class until the wizard itself is freed, so a screen that is removed by one choice and restored by another comes back with its controls and fields as the user left them.

Existing Delphi/VCL Framework demonstration
Existing Delphi/VCL Framework demonstration

This retained screenshot belongs to the repository's Delphi/VCL wizard demonstration. It is separate from the downloadable sample, which drives its wizard from code.

5. Validate before leaving a screen

Validate returns False and a message to block Next. OnNext runs only after validation succeeds, which makes it a safe place to copy accepted input into the model.

Pascal
function TDetailScreen.Validate(out AMessage: String): Boolean;
begin
  Result := Draft <> '';
  if Result then
    AMessage := ''
  else
    AMessage := 'Enter a value';
end;

procedure TDetailScreen.OnNext;
begin
  inherited;
  Model.Detail := Draft;
end;

6. Make Finish retryable

When Next is pressed on the last screen, or Finish is called, the wizard calls FinishRequested. This is where the application performs the operation the wizard was collecting input for. The sample stands in for that operation with a flag on the model.

Pascal
function TSampleWizard.FinishRequested(out AMessage: String): Boolean;
begin
  Result := Model.CanFinish;
  if Result then
    AMessage := ''
  else
    AMessage := 'Not ready to finish';
end;

Returning True moves State to wsFinished, closes the host and fires FinishedEvent. Returning False with a message returns the wizard to wsActive on the same screen, so the user can correct the problem and press Finish again. An exception raised inside FinishRequested also releases the wizard for another attempt. CancelRequested works the same way for wsCancelled, and IsCancellable can forbid cancelling altogether.

The states are the values of TWizardState. The labels on the two transitional states refer to the result of FinishRequested and CancelRequested.

7. Drive the wizard from code

Each navigation command has a form that reports its outcome instead of showing a message.

Method Result
TryNext, TryPrevious Return False and a message when navigation was rejected.
Next, Previous Call the Try forms and pass a rejection message to ShowError.
Finish, Cancel Return whether the wizard reached its terminal state.

CurrentScreen, Screens and State expose the current position, and ChangedEvent fires when the path or state changes. The sample uses these to run without user input. It overrides the protected ShowHost method, which normally shows the modal host, and calls the navigation methods directly.

Pascal
  Check(TryNext(Message), Message);
  Check(Length(Screens) = 3, 'Path should include the detail screen');
  Detail := TDetailScreen(CurrentScreen);
  Check(not TryNext(Message), 'Empty input should reject Next');

8. Limits

â„šī¸ Navigation and path changes belong on the UI thread, and FinishRequested must not navigate.
âš ī¸ Keep these limits in mind.
  • UpdatePath is intended for OnNext and OnPrevious. Calling it while the wizard is finishing or after it has ended raises an exception.
  • The wizard never frees the model. A class instance passed to Start remains the caller's to release.
  • FinishRequested is an ordinary method call. The wizard does not make the operation behind it transactional or repeatable, so an operation that can partly succeed needs its own recovery.
  • The sample drives real forms through an overridden ShowHost. It verifies routes, retained screen state and the retry, and it is not a demonstration of the interactive host.
Plain text
Path with detail=3 screens; without=2
Restored screen kept its input
Finish rejected once, then accepted

The wizard unit declares every type used here.

9. Get the Framework and run the example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus or Delphi runtime package.

The sample was compiled and run on Windows with Free Pascal 3.2.0 and the Lazarus Win32 widgetset, against source revision 2cc6558 . Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths.


Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .

Author: Herman Schoenfeld