How to Build a Wizard That Changes Its Route at Runtime

A setup wizard is rarely a fixed slide show. A user who selects an advanced option may need another page, while a straightforward request can go directly to the summary. Going Back should preserve the earlier screen's state, and a failed final operation should allow a retry instead of forcing the user to start again.

Sphere 10 Pascal Framework models this as one wizard with typed screen forms, a shared model and a mutable route. The route contains screen classes, while the wizard owns their instances. Our example starts with one screen, selects a details branch, returns to the first step, chooses the shorter branch and retries a rejected finish.

A multi-step wizard with alternative routes through its screens.
A multi-step wizard with alternative routes through its screens.

Contents

Put Shared Values in a Model

TWizardForm<TRequest> gives each screen the same typed TRequest model. The model holds the branch choice and the number of finish attempts. A real application would put validated input there rather than reading controls from a form that is no longer visible.

The wizard owns its screen instances, including screens removed from the current route. The caller owns this class-based model. Keep it alive until the wizard is finished and destroyed. Removing a class from the route is not a signal to free the corresponding form yourself.

Construct the Continuation After Validation

The first screen advertises wstAssumeHasNext, because its accepted OnNext will supply a continuation. It uses ptReplaceAllNext to choose Details then Summary, or Summary alone. Route selection happens in the screen's navigation lifecycle, not in a second independent button handler that bypasses validation.

The executable example overrides the modal presentation hook to exercise navigation deterministically without asking a reader to click a dialog during verification. The normal wizard uses its native host. This hook automates the sample's navigation while retaining the Framework's screen, validation and ownership implementation.


program ArticleSample;
{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF}
uses
  {$IFDEF FPC}Interfaces, Forms,{$ELSE}Vcl.Forms,{$ENDIF}
  Classes, SysUtils, UWizard;
type
  TRequest = class
  public
    NeedsDetails: Boolean;
    Attempts: Integer;
  end;
  TStartScreen = class(TWizardForm<TRequest>)
  public
    constructor Create(AOwner: TComponent); override;
    procedure OnNext; override;
  end;
  TDetailsScreen = class(TWizardForm<TRequest>)
  public
    constructor Create(AOwner: TComponent); override;
  end;
  TSummaryScreen = class(TDetailsScreen);
  TRequestWizard = class(TWizard<TRequest>)
  protected
    function FinishRequested(out AMessage: String): Boolean; override;
    function CancelRequested(out AMessage: String): Boolean; override;
    procedure ShowHost; override;
  end;

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

constructor TDetailsScreen.Create(AOwner: TComponent);
begin
  inherited CreateNew(AOwner);
end;

procedure TStartScreen.OnNext;
begin
  inherited;
  if Model.NeedsDetails then
    UpdatePath(ptReplaceAllNext, [TDetailsScreen, TSummaryScreen])
  else
    UpdatePath(ptReplaceAllNext, [TSummaryScreen]);
end;

function TRequestWizard.FinishRequested(out AMessage: String): Boolean;
begin
  Inc(Model.Attempts);
  Result := Model.Attempts > 1;
  if Result then AMessage := '' else AMessage := 'Please review and retry';
end;

function TRequestWizard.CancelRequested(out AMessage: String): Boolean;
begin
  AMessage := '';
  Result := True;
end;

procedure TRequestWizard.ShowHost;
var AMessage: String;
begin
  if not TryNext(AMessage) then raise Exception.Create(AMessage);
  if Length(Screens) <> 3 then raise Exception.Create('Details branch missing');
  if not TryPrevious(AMessage) then raise Exception.Create(AMessage);
  Model.NeedsDetails := False;
  if not TryNext(AMessage) then raise Exception.Create(AMessage);
  if Length(Screens) <> 2 then raise Exception.Create('Summary branch missing');
  if Finish(AMessage) then raise Exception.Create('First finish should be rejected');
  if State <> wsActive then raise Exception.Create('Rejected finish did not stay active');
  if not Finish(AMessage) then raise Exception.Create(AMessage);
  WriteLn('Details branch=3; summary branch=2; finish attempts=2');
end;

var Request: TRequest; Wizard: TRequestWizard;
begin
  Application.Initialize;
  Request := TRequest.Create;
  try
    Request.NeedsDetails := True;
    Wizard := TRequestWizard.Create(nil, [TStartScreen]);
    try
      Wizard.Start(Request);
      if Wizard.State <> wsFinished then raise Exception.Create('Finish failed');
    finally
      Wizard.Free;
    end;
  finally
    Request.Free;
  end;
end.

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

Original Delphi/VCL screenshot from the Framework repository, preserved without image editing.

Follow the Observed Route


Details branch=3; summary branch=2; finish attempts=2

The first accepted Next creates a three-screen path. After Previous, the same start-screen instance selects a two-screen path. A rejected Finish leaves the state active, so the second attempt can succeed. The sample checks the route lengths and terminal state rather than assuming that calling a finish method always closes the flow.

The repository's native wizard screenshot shows the related interactive showcase. Its launcher lets you test validation, optional notes, route injection/replacement, a confirmation boundary and simulated failures. The image is an existing Delphi/VCL demonstration, while this article's automated program was checked separately with Free Pascal.

Keep Completion Retryable and Deliberate

FinishRequested returns a Boolean and a message. Returning false is an ordinary rejected completion, not a completed transaction. The sample rejects the first call deliberately so the retry can be seen. In a real application, the callback should perform its approved business operation and report its actual outcome.

Do not equate wizard validation with a database transaction. If the final action can partially affect an external system, its own idempotency and recovery design still matters. The wizard supplies lifecycle admission and retryable presentation, while atomicity remains a property of the external operation.

ℹ️ Navigation, path changes and screen controls belong on the UI thread. Do not drive a wizard concurrently from worker completion callbacks without marshalling them through the application's normal UI boundary.

Design the Route as Part of the Product

A route should explain a useful workflow. Use an optional details page because a choice requires more information, not because dynamic navigation is available. Preserve useful Back behavior and make irreversible confirmation boundaries explicit. The user should be able to understand why a step appeared or disappeared.

Use ptInject for an added continuation, replacement modes for a revised path and removal only for the documented future-route case. Keep screen initialization separate from each presentation because a retained screen may be presented more than once without becoming a newly created object.

The wizard implementation documents those operations, and the interactive showcase demonstrates them together. This is a reusable workflow host rather than a stack of unrelated modal forms, leaving your application free to focus on the decisions and data that make the wizard useful.

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 .lpk or Delphi runtime package. Pascal uses these source/package workflows. The C# Framework is distributed separately through NuGet.

The attached application sample was compiled and executed with Free Pascal 3.2.0 on Windows against this public source revision , with range and overflow checks. Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths so the example does not need machine-specific paths. Lazarus UI examples require the LCL Win32 widgetset. A successful FPC run does not claim a new Delphi build. Follow the repository's separate Delphi/VCL project and resource instructions there.

Download the complete runnable sample.

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

Continue the Series

How to Give Pascal Resources Automatic Cleanup

How to Share a Desktop Component Between Delphi and Lazarus

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