Tutorials

Custom UI

15 min read · Prerequisite: Getting Started · Wraps demo/11-custom-ui-simple, demo/12-custom-ui-vstyle, demo/13-glass-ui

FalkForge includes a built-in WPF UI framework for installer applications. Instead of the standard MSI dialog boxes, you can build fully custom installer experiences with pages, navigation, theming, and data binding.

WPF (Windows Presentation Foundation) is a .NET UI framework for desktop apps. It uses XAML markup to define layouts and supports rich styling, animations, and data binding. You don't need deep WPF knowledge to follow this tutorial — we explain each concept as it appears.

1. Your First Custom UI

Open demo/11-custom-ui-simple/Program.cs. The entire app is a single call to InstallerApp.Run().

The entry point

using CustomUiSimple.Pages;
using FalkForge.Ui;

return InstallerApp.Run(args, app => app
    .Localization(loc => loc
        .DefaultCulture("en-US")
        .AddJsonResources()
        .DetectCulture()
        .AllowLanguageSelection())
    .Window(w => w
        .Size(500, 350)
        .Title("My App Setup")
        .Accent("#2563EB"))
    .Pages(p => p
        .Add<WelcomePage>()
        .Add<ProgressPage>()
        .Add<CompletePage>())));

InstallerApp.Run() is the entry point for every custom UI installer. It launches the WPF application, connects to the engine process, and renders your pages. The return passes the exit code back to the OS.

Three builders configure the app:

Pages with InstallerPage<TView>

Each page is a C# class paired with a XAML view. The class holds logic and data. The view defines the layout. WPF data binding connects the two.

public class WelcomePage : InstallerPage<WelcomeView>
{
    public override string Title => Localize("Welcome.Title");
    public string ProductName => Localize("Welcome.ProductName");
    public string Description => Localize("Welcome.Description");

    public override bool CanGoBack => false;

    public override PageResult OnNext()
    {
        return PageResult.Next;
    }
}

Key points:

The XAML view

The view is a standard WPF UserControl. Properties on the page class are available via data binding.

<UserControl x:Class="CustomUiSimple.Views.WelcomeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Padding="32">
    <StackPanel VerticalAlignment="Center">
        <TextBlock Text="{Binding ProductName}"
                   FontSize="24" FontWeight="SemiBold" />
        <TextBlock Text="{Binding Description}"
                   FontSize="14" TextWrapping="Wrap" />
    </StackPanel>
</UserControl>

{Binding ProductName} reads the ProductName property from the page. When the property changes, WPF updates the text automatically. This is data binding — the view never touches the page directly.

Page navigation

Navigation is driven by PageResult values returned from OnNext() and OnBack(). The framework handles the rest.

In demo 11, the flow is:

  1. WelcomePage — returns PageResult.Next to advance.
  2. ProgressPage — returns PageResult.Install to trigger the installation.
  3. CompletePage — returns PageResult.Finish to close the installer.
Deep dive: PageResult options

PageResult has these built-in values:

  • Next — advance to the next registered page.
  • Previous — go back one page.
  • Finish — close the installer window.
  • Cancel — abort the installation.
  • Install — start the install action.
  • Uninstall — start the uninstall action.
  • Repair — start the repair action.

Two factory methods offer more control:

  • Stay(message?) — remain on the current page. Optionally display a validation message.
  • GoTo<TPage>() — jump to a specific page by type, skipping the normal order.

Combine these to build any navigation pattern. For example, a workloads page can call PageResult.Stay("Select at least one workload") to block progress until the user makes a selection.

Registering pages

Pages are registered in the Pages() builder. The order matters — it defines the sequence the user navigates through.

.Pages(p => p
    .Add<WelcomePage>()
    .Add<ProgressPage>()
    .Add<CompletePage>())

FalkForge creates each page instance, binds its view, and manages the page stack. You never instantiate pages yourself.

Deep dive: The 3-process architecture

When a custom UI installer runs, FalkForge launches three processes:

  1. UI process — your WPF application. Renders pages, collects user input, displays progress.
  2. Engine process — a NativeAOT executable. Detects installed packages, plans actions, and coordinates execution.
  3. Elevated process — a NativeAOT executable running as administrator. Performs privileged operations like writing to Program Files and modifying the registry.

The processes communicate over named pipes with HMAC-SHA256 authentication. Your page code talks to the engine through the Engine property on InstallerPage. The engine handles elevation transparently — you never launch processes or manage pipes yourself.

This architecture keeps the UI responsive (no admin prompts blocking the window) and limits the attack surface (only the elevated process has admin rights).

Try it

dotnet run --project demo/11-custom-ui-simple

You'll see a window with a welcome page, a progress page, and a completion page. Three pages, zero MSI dialog boxes.

2. VS-Style Dark Theme

Demo 12 builds a Visual Studio-style installer with a dark theme, borderless window, and a workload selector. Open demo/12-custom-ui-vstyle/Program.cs.

Window configuration

.Window(w => w
    .Size(1024, 700)
    .Borderless()
    .Background("#1E1E1E")
    .Accent("#7B68EE")
    .Title("FalkForge DevTools Suite Installer"))

The InstallerWindowBuilder controls the window chrome:

Richer pages

Demo 12 has four pages: ProductPage, WorkloadsPage, ProgressPage, and CompletePage.

The product page detects whether the app is already installed and adjusts its UI accordingly:

public bool IsInstalled => DetectedState == InstallState.Installed;
public bool IsNotInstalled => DetectedState == InstallState.NotInstalled;

DetectedState is a built-in property on InstallerPage. The engine detects whether the product is installed, being modified, or absent. Your view can bind to these properties to show Install, Modify, or Repair buttons as needed.

Validation with Stay()

The workloads page uses PageResult.Stay() to prevent navigation when no workload is selected:

public override PageResult OnNext()
{
    if (!Workloads.Any(w => w.IsSelected))
        return PageResult.Stay(Localize("Workloads.SelectAtLeastOne"));
    return PageResult.Install;
}

The message is displayed to the user. The page stays put until the validation passes.

Observable collections

The workloads page uses ObservableCollection<Workload> to hold its data. When items are added or removed, WPF updates the list automatically. The page also uses SetField() for property change notifications:

public Workload? SelectedWorkload
{
    get => _selectedWorkload;
    set => SetField(ref _selectedWorkload, value);
}

SetField() is provided by InstallerPage. It sets the backing field and raises PropertyChanged so the view updates. Use it for any property the UI binds to.

Deep dive: How DynamicResource theming works

FalkForge's built-in theme uses WPF DynamicResource keys. When you call Background() or Accent() on the window builder, FalkForge updates the application-level resource dictionary at runtime.

Unlike StaticResource (resolved once at load time), DynamicResource re-evaluates whenever the resource changes. This means:

  • You can switch themes at runtime without restarting the app.
  • Custom pages automatically pick up the accent colour if they reference the theme keys.
  • The built-in navigation bar, buttons, and progress indicators all respond to your colour choices.

The theme is defined in InstallerTheme.xaml. Key resource names include accent brushes, background brushes, and text foreground brushes. Your XAML views can reference these keys to stay consistent with the overall theme.

Try it

dotnet run --project demo/12-custom-ui-vstyle

A dark, borderless window appears. Browse the workload selector, toggle components, and watch the total size update in real time.

3. Glass Effect UI

Demo 13 pushes the visual boundaries. It replaces the default installer window with a fully custom WPF window featuring a translucent glass effect, rounded corners, and glow effects.

Open demo/13-glass-ui/Program.cs:

return InstallerApp.Run(args, app => app
    .Window(w => w
        .CustomWindow<GlassWindow>()
        .Size(500, 350)
        .Borderless()
        .Title("GlassForge"))
    .Pages(p => p
        .Add<InstallPage>()));

The key difference is CustomWindow<GlassWindow>(). Instead of the default installer window, FalkForge uses your custom WPF Window class.

The custom window

The GlassWindow is a standard WPF Window with transparency enabled:

<Window WindowStyle="None"
        AllowsTransparency="True"
        Background="Transparent">
    <Border CornerRadius="175" ClipToBounds="True">
        <Border.Background>
            <SolidColorBrush Color="#1A1A2E" Opacity="0.85" />
        </Border.Background>
        <ContentPresenter Content="{Binding CurrentView}" />
    </Border>
</Window>

Three WPF properties create the glass effect:

The Border with a large CornerRadius creates the pill shape. Its semi-transparent background (Opacity="0.85") lets the desktop show through slightly. A gradient BorderBrush adds the subtle edge glow.

The ContentPresenter binds to CurrentView, which FalkForge sets to the active page's view.

Window dragging

Without a title bar, the user needs another way to move the window. The code-behind adds drag support:

MouseLeftButtonDown += (_, e) =>
{
    if (e.OriginalSource is not (Button or TextBlock))
        DragMove();
};

Clicking anywhere on the window background starts a drag. Clicks on buttons and text blocks are excluded so they behave normally.

Visual effects in views

The install view uses WPF effects for a neon glow style:

<TextBlock Text="GlassForge" Foreground="White" FontSize="28">
    <TextBlock.Effect>
        <DropShadowEffect Color="#00D4FF" BlurRadius="15"
                           ShadowDepth="0" Opacity="0.3" />
    </TextBlock.Effect>
</TextBlock>

A DropShadowEffect with ShadowDepth="0" creates a glow behind the text instead of a traditional shadow. The button uses the same technique with a hover trigger that intensifies the glow.

Driving the engine directly

Demo 13's install page calls the engine API directly instead of returning PageResult.Install:

public async Task InstallAsync()
{
    await Engine.PlanAsync(InstallAction.Install);
    await Engine.ApplyAsync();
}

The Engine property on InstallerPage gives you direct access to the installer engine. PlanAsync() builds the execution plan. ApplyAsync() runs it. This gives you full control over when and how installation happens — useful for single-page installers or custom progress reporting.

Deep dive: When to use custom UI vs built-in templates

FalkForge offers two paths for installer UI:

ApproachBest forEffort
Built-in dialog templates
(Minimal, InstallDir, FeatureTree, Mondo, Advanced)
Standard installers. IT-deployed software. Products where users expect a familiar install experience. One line of code: UseDialogSet()
Custom UI
(InstallerApp.Run() + InstallerPage<T>)
Branded experiences. Complex workflows (workload selection, license activation). Products where the installer is the first impression. XAML views + page classes. More code, full control.

You can start with a built-in template and migrate to custom UI later. The underlying engine and compilation pipeline are the same — only the UI layer changes.

Try it

dotnet run --project demo/13-glass-ui

A translucent pill-shaped window appears with a glowing install button. This is the same engine and pipeline — just a radically different surface.

Recap

You've seen three levels of UI customization:

  1. Demo 11 — standard window with custom pages. The simplest custom UI. Good starting point.
  2. Demo 12 — borderless dark theme with workload selection. Production-ready branded installer.
  3. Demo 13 — fully custom window with glass effects. Shows that anything WPF can render, FalkForge can host.

Choosing the right approach: