Skip to content

Windows

Most Microsoft.UI.Reactor (Reactor) apps start with a single window — the one ReactorApp.Run opens for you. When you need more (a settings dialog, a multi-document app, a tray flyout), the ReactorApp static surface and a handful of hooks give you a declarative way to manage every top-level window in the process.

The Primary Window

ReactorApp.Run<TRoot>(...) opens one window with TRoot mounted as its content:

ReactorApp.Run<WindowsApp>("Windows Demo", width: 640, height: 520
#if DEBUG
    , preview: true
#endif
);

That window becomes ReactorApp.PrimaryWindow. By default, when the primary window closes the process exits — see Shutdown Policy below if you need different behavior.

Opening a Secondary Window

Call ReactorApp.OpenWindow(spec, factory) from anywhere on the UI thread. The WindowSpec describes the window's chrome (title, size, icon, presenter); the factory builds the root component:

class WindowsApp : Component
{
    public override Element Render()
    {
        var (count, setCount) = UseState(0);

        return VStack(12,
            Heading("Top-level Windows"),
            HStack(8,
                Button("New Notepad window", () =>
                {
                    var n = count + 1;
                    ReactorApp.OpenWindow(
                        new WindowSpec
                        {
                            Title = $"Notepad #{n}",
                            Width = 420,
                            Height = 300,
                        },
                        () => new NotePadWindow($"Document #{n}"));
                    setCount(n);
                }),
                Button("Open settings", () =>
                {
                    // Reuse the same window if it's already open: FindWindow
                    // looks the surface up by its WindowKey, so a second
                    // click brings the existing window forward instead of
                    // opening a duplicate.
                    var key = WindowKey.Of("settings");
                    var existing = ReactorApp.FindWindow(key);
                    if (existing is not null)
                    {
                        existing.Activate();
                        return;
                    }

                    ReactorApp.OpenWindow(
                        new WindowSpec
                        {
                            Title = "Settings",
                            Width = 480,
                            Height = 360,
                            Key = key,
                        },
                        () => new SettingsWindow());
                })
            ),
            TextBlock($"Open windows: {ReactorApp.Windows.Count}")
        ).Padding(20);
    }
}

Top-level windows demo

Each click of "New Notepad window" spawns an independent window. They have their own state, their own DPI, their own lifecycle — closing one doesn't affect the others.

WindowSpec is an immutable record. The fields you'll reach for most often:

Field Default Purpose
Title "Reactor App" Caption text
Width / Height 1024 / 768 Initial size in DIPs
MinWidth / MinHeight null Optional resize floor
Icon null WindowIcon.FromPath(...) or WindowIcon.FromResource(...)
StartPosition Default CenterOnPrimary, CenterOnOwner, Manual, RestoreFromPersistence
PersistenceId null Save and restore placement across sessions
Key null Stable identity for FindWindow and UseOpenWindow
Owner null Owner window for owned-window semantics

All sizes are DIPs (device-independent pixels). Reactor handles the DIP→physical conversion at the destination monitor's DPI.

Reading the Current Window

Inside any component rendered as a window's content, hooks reach the owning ReactorWindow:

class NotePadWindow : Component
{
    private readonly string _label;
    public NotePadWindow(string label) { _label = label; }

    public override Element Render()
    {
        var (text, setText) = UseState("");
        var window = UseWindow();
        var state = UseWindowState();

        return VStack(12,
            SubHeading(_label),
            TextBlock(window is null
                ? "(no owning window)"
                : $"id={window.Id}  state={state}  dpi={window.Dpi}"),
            TextBox(text, setText, placeholderText: "Type something...")
                .Width(360),
            Button("Close", () => window?.Close())
        ).Padding(16);
    }
}
Hook Purpose
UseWindow() The owning ReactorWindownull outside a window (e.g. tray flyouts)
UseWindowState() Normal / Minimized / Maximized / FullScreen / CompactOverlay; re-renders on change
UseIsActive() true while the window has foreground; re-renders on activation change
UseDpi() Current DPI; re-renders on monitor change
UseClosingGuard(canClose) Register a synchronous predicate that can veto a close

ReactorWindow itself exposes Close(), Activate(), SetSize(...), SetPosition(...), and lifecycle events (Closing, Closed, SizeChanged, StateChanged).

Finding and Enumerating Windows

ReactorApp.Windows          // IReadOnlyList<ReactorWindow> snapshot
ReactorApp.PrimaryWindow    // First window opened, or null after it closes
ReactorApp.FindWindow(key)  // Look up by WindowKey

WindowKey is a string-typed identity stamped onto a WindowSpec.Key. Use it to dedupe "open the settings window" against an existing one, as the button handler in the shell snippet above does.

Reusing a Window with UseOpenWindow

UseOpenWindow lets a component declaratively own a window's lifetime: while the component is mounted, the window stays open; renders that pass the same WindowKey reuse the same handle.

class SettingsHost : Component
{
    public override Element Render()
    {
        // While this component is mounted, ensure a settings window keyed
        // to "settings" is open. Re-renders that pass the same WindowKey
        // reuse the same handle; the hook dedupes against the live window
        // registry via FindWindow.
        var settings = UseOpenWindow(
            key: "settings",
            spec: new WindowSpec { Title = "Settings", Width = 480, Height = 360 },
            factory: () => new SettingsWindow());

        return TextBlock(settings is null
            ? "(no UI dispatcher)"
            : $"Settings open — id={settings.Id}");
    }
}

UseOpenWindow doesn't unmount-close the window — that's deliberate, since windows usually outlive the menu item that opened them. To close on unmount, return a UseEffect cleanup that calls Close() on the handle.

(Tray icons go the other way — UseTrayIcon does close on unmount, since a tray icon belongs to the component that declared it.)

Shutdown Policy

// Call once at startup, before ReactorApp.Run. With OnLastSurfaceClosed the
// process keeps running while a tray icon or any window is alive; with
// Explicit you must call ReactorApp.Exit() yourself.
static class Startup
{
    public static void ConfigureShutdown()
    {
        ReactorApp.ShutdownPolicy = ShutdownPolicy.OnLastSurfaceClosed;
    }
}
Policy Process exits when...
OnPrimaryWindowClosed (default) The primary window closes
OnLastSurfaceClosed The last window AND the last tray icon both close
Explicit Never automatically — you must call ReactorApp.Exit()

Pick OnLastSurfaceClosed for tray-resident apps that should keep running while only an icon is visible. Pick Explicit when you need a custom "quit" gesture (a tray menu item, a Cmd-Q accelerator, etc.) and call ReactorApp.Exit(exitCode) from the handler.

Tray Icons (in Brief)

A tray icon is a non-window surface registered with the shell's notification area. Use UseTrayIcon from a component to register one whose lifetime is tied to that component:

class TrayHost : Component
{
    public override Element Render()
    {
        var icon = UseMemo(() => WindowIcon.FromPath("Assets/TrayIcon.ico"));
        var tray = UseTrayIcon(new TrayIconSpec(
            Icon: icon,
            Tooltip: "My App",
            Key: WindowKey.Of("main-tray")));

        UseEffect(() =>
        {
            if (tray is null) return () => { };
            void onClick(object? s, EventArgs e)
                => ReactorApp.PrimaryWindow?.Activate();
            tray.Click += onClick;
            return () => tray.Click -= onClick;
        }, tray ?? (object)"no-tray");

        return TextBlock("Tray icon registered while this component is mounted.");
    }
}

The hook returns a ReactorTrayIcon? exposing Click, DoubleClick, and RightClick events plus ShowFlyout(content) for in-place flyout UI. For app-scoped tray icons that survive component unmount, call ReactorApp.OpenTrayIcon(spec) directly.

Tips

One spec record per window — don't rebuild it in Render() if you can avoid it. Wrap the WindowSpec (and especially the WindowIcon) in a UseMemo so re-renders don't reallocate them. Identity-stable specs let UseOpenWindow's value-equality compare see no change and skip the chrome-update pass.

Width/Height are doubles, not ints. All sizes and positions are DIPs. Literal call sites bind cleanly (Width = 480), but a variable typed int needs a cast.

Use WindowKey for any window you might want to find again. The cost is one string allocation; the payoff is FindWindow lookups, UseOpenWindow reuse, and shell tools that can address the surface by name.

Closing guards are synchronous. UseClosingGuard(() => …) runs on the UI thread and must return immediately. For an async confirmation dialog, return false from the guard and re-trigger Close() from the dialog callback.

The primary window is special only by default. Once you change ShutdownPolicy, "primary" is just a name for the first window you opened — there's nothing the framework treats differently about it.

Next Steps

  • WinForms Interop — previous topic: hosting Reactor inside WinForms via XAML Islands
  • Reactor — back to the index: overview of the framework and full topic list
  • Navigation — in-window routing with UseNavigation, NavigationView, deep links
  • Effects and LifecycleUseEffect patterns for the imperative work that pairs with windows
  • Commanding — wire keyboard accelerators and tray-menu items to commands