SharpConsoleUI Patterns Cookbook

Practical recipes for building TUI applications with SharpConsoleUI. Each pattern shows the real way it's done in production cx apps (cxfiles, cxtop, LazyNuGet).

See also: Threading & Async — the UI thread model, async events, and the unresponsive watchdog.

1. App Bootstrap

Every cx app follows the same startup pattern:

// Create driver with buffer rendering (always use Buffer mode)
var driver = new NetConsoleDriver(RenderMode.Buffer);

// Create window system — disable default panels for full control
var ws = new ConsoleWindowSystem(driver,
    options: new ConsoleWindowSystemOptions(
        ShowTopPanel: false,
        ShowBottomPanel: false,
        WindowCycleKey: null));  // null to handle tab switching yourself

// Create and show main window
var mainWindow = new WindowBuilder(ws)
    .WithTitle("My App")
    .Maximized()
    .AddControls(toolbar, mainGrid, statusBar)
    .WithAsyncWindowThread(UpdateLoopAsync)
    .OnKeyPressed(OnGlobalKeyPressed)
    .BuildAndShow();

// Run the event loop (blocks until shutdown)
await Task.Run(() => ws.Run());

With Desktop Panels (Start Menu, TaskBar, Clock)

var options = new ConsoleWindowSystemOptions(
    TopPanelConfig: panel => panel
        .Left(Elements.StatusText("[bold cyan]My App[/]"))
        .Left(Elements.Separator())
        .Right(Elements.Performance()),
    BottomPanelConfig: panel => panel
        .Left(Elements.StartMenu()
            .WithText("☰ Menu")
            .WithOptions(new StartMenuOptions
            {
                AppName = "My App",
                SidebarStyle = StartMenuSidebarStyle.IconLabel
            }))
        .Center(Elements.TaskBar())
        .Right(Elements.Clock().WithFormat("HH:mm:ss"))
);
var ws = new ConsoleWindowSystem(new NetConsoleDriver(RenderMode.Buffer), options: options);

2. Split Layout with Resizable Splitter

Two-panel layout (sidebar + main content):

var grid = Controls.HorizontalGrid()
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithAlignment(HorizontalAlignment.Stretch)
    .Column(col => col.Width(40).Add(sidebarPanel))   // Fixed width
    .Column(col => col.Flex(1).Add(contentPanel))      // Fill remaining
    .WithSplitterAfter(0)                              // Draggable splitter
    .Build();

Three-panel layout (explorer + editor + side panel):

var grid = Controls.HorizontalGrid()
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithAlignment(HorizontalAlignment.Stretch)
    .Column(col => col.Width(25).Add(explorerPanel))
    .Column(col => col.Flex(1).Add(editorPanel))
    .Column(col => col.Width(30).Add(detailPanel))
    .WithSplitterAfter(0)
    .WithSplitterAfter(1)
    .Build();

Toggling a Column at Runtime

bool detailVisible = true;

void ToggleDetailPanel()
{
    detailVisible = !detailVisible;
    var columns = grid.Columns;
    var splitters = grid.Splitters;
    
    if (columns.Count >= 3)
        columns[2].Visible = detailVisible;
    if (splitters.Count >= 2)
        splitters[1].Visible = detailVisible;
    
    mainWindow.ForceRebuildLayout();
    mainWindow.Invalidate(Invalidation.Relayout);
}

3. Async Data Updates (Background → UI)

Pattern A: Async Window Thread (Continuous Updates)

Best for dashboards, monitors, real-time data:

var window = new WindowBuilder(ws)
    .WithAsyncWindowThread(UpdateLoopAsync)
    .BuildAndShow();

private async Task UpdateLoopAsync(Window window, CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        try
        {
            var data = await FetchDataAsync(ct);
            
            // Update controls directly — this runs on the UI context
            window.FindControl<SparklineControl>("cpuSparkline")?.SetDataPoints(data.CpuHistory);
            
            var bar = window.FindControl<BarGraphControl>("cpuBar");
            if (bar != null) bar.Value = data.CpuPercent;
            
            var status = window.FindControl<MarkupControl>("status");
            status?.SetContent(new List<string> { $"[green]CPU: {data.CpuPercent:F1}%[/]" });
        }
        catch (Exception) { /* continue on error */ }

        try { await Task.Delay(1000, ct); }
        catch (TaskCanceledException) { break; }
    }
}

Pattern B: Fire-and-Forget with Cancellation

Best for user-triggered async operations (search, load, fetch):

private CancellationTokenSource? _loadCts;

public void LoadPackageDetails(string packageId)
{
    // Cancel previous load
    var previousCts = _loadCts;
    _loadCts = new CancellationTokenSource();
    try { previousCts?.Cancel(); } catch (ObjectDisposedException) { }

    var ct = _loadCts.Token;
    ShowLoadingState();

    AsyncHelper.FireAndForget(async () =>
    {
        var data = await _service.GetDetailsAsync(packageId, ct);
        if (ct.IsCancellationRequested) return;
        
        // Update UI with result
        UpdateDetailsPanel(data);
    },
    ex => ShowError(ex.Message));
}

Pattern C: EnqueueOnUIThread (From Any Thread)

Best for file watchers, timer callbacks, external events:

// File watcher callback — runs on background thread
fileWatcher = fileSystem.WatchDirectory(path, _ =>
{
    ws.EnqueueOnUIThread(() =>
    {
        RefreshFileList();
        UpdateStatusBar();
    });
});

// Note: Container?.Invalidate(Invalidation work) is the ONLY call safe to make directly
// from a background thread without EnqueueOnUIThread — it folds the request into the
// window's PendingWork accumulator via a lock-free atomic max-join.

4. Modal Dialog with Result

Base Pattern (TaskCompletionSource)

public abstract class ModalBase<TResult>
{
    private TaskCompletionSource<TResult>? _tcs;
    protected Window? Modal { get; private set; }
    protected ConsoleWindowSystem WindowSystem { get; }

    public Task<TResult> ShowAsync()
    {
        _tcs = new TaskCompletionSource<TResult>();
        
        Modal = new WindowBuilder(WindowSystem)
            .WithTitle(GetTitle())
            .WithSize(GetWidth(), GetHeight())
            .Centered()
            .AsModal()
            .WithBorderStyle(BorderStyle.Rounded)
            .OnKeyPressed((s, e) =>
            {
                if (e.KeyInfo.Key == ConsoleKey.Escape)
                {
                    CloseWithResult(GetDefaultResult());
                    e.Handled = true;
                }
            })
            .Build();

        BuildContent();  // Derived class adds controls
        WindowSystem.AddWindow(Modal);
        WindowSystem.SetActiveWindow(Modal);
        return _tcs.Task;
    }

    protected void CloseWithResult(TResult result)
    {
        WindowSystem.CloseWindow(Modal!);
        _tcs?.TrySetResult(result);
    }

    protected abstract string GetTitle();
    protected abstract void BuildContent();
    protected abstract TResult GetDefaultResult();
    protected virtual int GetWidth() => 60;
    protected virtual int GetHeight() => 18;
}

Confirm Dialog Usage

// Define
public class ConfirmModal : ModalBase<bool>
{
    public static Task<bool> ShowAsync(ConsoleWindowSystem ws, string title, string message)
        => new ConfirmModal(ws, title, message).ShowAsync();
    
    protected override void BuildContent()
    {
        var label = Controls.Markup().AddLine(_message).Build();
        var yesBtn = Controls.Button().WithText("  Yes  ")
            .OnClick((_, _) => CloseWithResult(true)).Build();
        var noBtn = Controls.Button().WithText("  No  ")
            .OnClick((_, _) => CloseWithResult(false)).Build();
        Modal!.AddControl(label);
        Modal!.AddControl(yesBtn);
        Modal!.AddControl(noBtn);
    }
}

// Use
var confirmed = await ConfirmModal.ShowAsync(ws, "Delete?", "Permanently delete this file?");
if (!confirmed) return;

5. Keyboard Shortcuts

Global Key Handler

private void OnGlobalKeyPressed(object? sender, KeyPressedEventArgs e)
{
    var key = e.KeyInfo;
    bool ctrl = key.Modifiers.HasFlag(ConsoleModifiers.Control);
    bool shift = key.Modifiers.HasFlag(ConsoleModifiers.Shift);

    // Check modified keys BEFORE plain keys
    switch (key.Key)
    {
        case ConsoleKey.T when ctrl:
            NewTab();
            e.Handled = true;
            break;

        case ConsoleKey.S when ctrl:
            _ = ShowSearchAsync();
            e.Handled = true;
            break;

        case ConsoleKey.F when ctrl:
            EnterFilterMode();
            e.Handled = true;
            break;

        case ConsoleKey.F2:
            _ = RenameSelectedAsync();
            e.Handled = true;
            break;

        case ConsoleKey.Escape:
            NavigateBack();
            e.Handled = true;
            break;
    }
}

// Register on window builder
var window = new WindowBuilder(ws)
    .OnKeyPressed(OnGlobalKeyPressed)
    .Build();

Preview Key Routing (Multi-Level)

For handling keys before controls consume them:

mainWindow.PreviewKeyPressed += (_, e) =>
{
    // Context menu gets first priority
    if (contextMenu.ProcessPreviewKey(e)) return;

    // Active portal gets next priority
    if (activePortal != null)
    {
        activePortal.ProcessKey(e.KeyInfo);
        e.Handled = true;
        return;
    }
};

6. Toolbar

Simple Toolbar

var toolbar = Controls.Toolbar()
    .StickyTop()
    .WithSpacing(1)
    .WithBackgroundColor(Color.Grey11)
    .WithBelowLine()
    .AddButton("New", (_, _) => CreateNew())
    .AddButton("Open", (_, _) => OpenFile())
    .AddButton("Save", (_, _) => SaveFile())
    .Build();

Dynamic Toolbar with Conditional Buttons

private void UpdateToolbar()
{
    toolbar.Clear();

    AddToolbarButton("◈ Open [grey50]Enter[/]", OpenSelected);
    AddToolbarButton("↑ Up [grey50]Bksp[/]", NavigateUp);

    if (canCreateTab)
        AddToolbarButton("❒ New Tab [grey50]^T[/]", NewTab);

    toolbar.AddItem(new SeparatorControl());

    if (hasSelection)
    {
        AddToolbarButton("✕ Delete [grey50]Del[/]", () => _ = DeleteAsync());
        AddToolbarButton("✎ Rename [grey50]F2[/]", () => _ = RenameAsync());
    }
}

private void AddToolbarButton(string label, Action action)
{
    var btn = Controls.Button()
        .WithText(label)
        .WithBorder(ButtonBorderStyle.None)
        .WithBackgroundColor(Color.Transparent)
        .OnClick((_, _) => action())
        .Build();
    toolbar.AddItem(btn);
}

7. Status Bar

Three-Zone Status Bar

var statusBar = Controls.StatusBar()
    .AddLeft("↑↓", "Navigate")
    .AddLeft("Enter", "View")
    .AddLeftSeparator()
    .AddLeft("Esc", "Exit")
    .AddCenterText("[dim]My App[/]")
    .AddRight("Ctrl+S", "Search")
    .AddRightText("[yellow]3 items[/]")
    .WithAboveLine()
    .WithBackgroundColor(Color.Grey15)
    .WithShortcutForegroundColor(Color.Cyan1)
    .StickyBottom()
    .Build();

Dynamic Status Updates with BatchUpdate

statusBar.BatchUpdate(() =>
{
    statusBar.ClearAll();
    
    statusBar.AddLeftText($"[dim]{itemCount} items[/]");
    if (selectedCount > 0)
    {
        statusBar.AddLeftSeparator();
        statusBar.AddLeftText($"[cyan]{selectedCount} selected[/]");
    }
    
    statusBar.AddRightText("[grey70]Refresh[/] [grey50]F5[/]",
        () => Refresh());
    statusBar.AddRightText($"[{detailColor}]Detail[/] [grey50]F3[/]",
        () => ToggleDetail());
});

8. ScrollablePanel with Live Content

var panel = Controls.ScrollablePanel()
    .WithVerticalScroll(ScrollMode.Scroll)
    .WithScrollbar(true)
    .WithMouseWheel(true)
    .WithAutoScroll(false)
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithColors(Color.Grey93, Color.Black)
    .Build();

// Add static content
panel.AddControl(Controls.Markup()
    .AddLine("[bold]Title[/]")
    .AddLine("[dim]Subtitle[/]")
    .WithMargin(1, 1, 1, 0)
    .Build());

// Add a button
panel.AddControl(Controls.Button()
    .WithText("  Click Me  ")
    .WithMargin(1, 1, 0, 0)
    .WithBorder(ButtonBorderStyle.Rounded)
    .OnClick((_, _) => DoSomething())
    .Build());

9. List with Selection and Live Updates

var list = Controls.List()
    .WithTitle("Items")
    .WithAlignment(HorizontalAlignment.Stretch)
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithColors(Color.Grey93, Color.Black)
    .WithHighlightColors(Color.White, Color.Grey35)
    .SimpleMode()
    .Build();

// Selection tracking
list.SelectedIndexChanged += (_, idx) =>
{
    if (idx >= 0 && idx < items.Count)
        ShowDetails(items[idx]);
};

// Activation (Enter key)
list.ItemActivated += (_, item) =>
{
    OpenItem(item);
};

// Dynamic updates
list.ClearItems();
foreach (var item in newItems)
    list.AddItem(new ListItem(item.DisplayMarkup) { Tag = item });
list.SelectedIndex = 0;

10. Progress and Resource Visualization

Bar Graph

var bar = new BarGraphBuilder()
    .WithName("cpuBar")
    .WithLabel("CPU")
    .WithLabelWidth(6)
    .WithValue(0)
    .WithMaxValue(100)
    .WithAlignment(HorizontalAlignment.Stretch)
    .WithSmoothGradient(new Color[] {
        new(0x4e, 0xcd, 0xc4),  // teal (low)
        new(0xff, 0xd9, 0x3d),  // yellow (mid)
        new(0xff, 0x6b, 0x6b)   // red (high)
    })
    .ShowLabel()
    .ShowValue()
    .WithValueFormat("F1")
    .WithMargin(1, 0, 1, 0)
    .Build();

// Update
bar.Value = 78.5;

Sparkline (Trend Over Time)

var sparkline = new SparklineBuilder()
    .WithName("cpuSparkline")
    .WithTitle("CPU %")
    .WithHeight(4)
    .WithMaxValue(100)
    .WithGradient(gradientColors)
    .WithMode(SparklineMode.Braille)
    .WithBaseline(true, position: TitlePosition.Bottom)
    .WithAlignment(HorizontalAlignment.Stretch)
    .WithMargin(1, 0, 1, 0)
    .WithData(historyData)
    .Build();

// Update with new data point
historyData.Add(newValue);
while (historyData.Count > 50)
    historyData.RemoveAt(0);
sparkline.SetDataPoints(historyData);

Progress Bar

var progress = Controls.ProgressBar()
    .WithAlignment(HorizontalAlignment.Stretch)
    .Build();

// Update (0.0 to 1.0)
progress.Value = 0.65;

11. Live Log Viewer

Pattern: Rolling Log with Timestamps

private readonly List<string> _logLines = new();
private readonly object _logLock = new();
private MarkupControl? _logContent;

private void AddLogLine(string message)
{
    lock (_logLock)
    {
        var elapsed = DateTime.Now - _startTime;
        var timestamp = $"[grey50]{elapsed.TotalSeconds:F1}s[/]";
        var logLine = $"{timestamp} [grey70]{MarkupParser.Escape(message)}[/]";

        _logLines.Add(logLine);

        // Keep last 500 lines
        if (_logLines.Count > 500)
            _logLines.RemoveAt(0);

        _logContent?.SetContent(new List<string>(_logLines));
    }
}

12. Tabs

var tabControl = new TabControlBuilder()
    .WithHeaderStyle(TabHeaderStyle.AccentedSeparator)
    .Fill()
    .WithAlignment(HorizontalAlignment.Stretch)
    .AddTab("Overview", overviewPanel)
    .AddTab("Details", detailsPanel)
    .AddTab("Logs", logsPanel)
    .Build();

tabControl.ActiveTabIndex = 0;

// Tab change event
tabControl.TabChanged += (_, e) =>
{
    OnTabChanged(e.NewIndex);
};

// Dynamic tabs (closable)
tabControl.AddTab("New Tab", contentPanel, isClosable: true);
private CancellationTokenSource? _searchCts;

private void OnSearchInputChanged(object? sender, string query)
{
    _searchCts?.Cancel();
    _searchCts = new CancellationTokenSource();
    var ct = _searchCts.Token;

    if (query.Length < 2) return;

    ShowSearchingIndicator();

    AsyncHelper.FireAndForget(async () =>
    {
        await Task.Delay(400, ct);  // 400ms debounce
        if (ct.IsCancellationRequested) return;

        var results = await _service.SearchAsync(query, ct);
        if (ct.IsCancellationRequested) return;

        UpdateResultsList(results);
        HideSearchingIndicator();
    },
    ex => HideSearchingIndicator());
}

14. Responsive Layout (Width-Based)

private ResponsiveLayoutMode _currentLayout;
private const int WideThreshold = 120;

public IWindowControl BuildPanel(int windowWidth)
{
    _currentLayout = windowWidth >= WideThreshold
        ? ResponsiveLayoutMode.Wide
        : ResponsiveLayoutMode.Narrow;

    return _currentLayout == ResponsiveLayoutMode.Wide
        ? BuildWideLayout()    // 3-column: text | separator | graphs
        : BuildNarrowLayout(); // 1-column: text then graphs stacked
}

public void HandleResize(int newWidth)
{
    var desired = newWidth >= WideThreshold
        ? ResponsiveLayoutMode.Wide
        : ResponsiveLayoutMode.Narrow;

    if (desired == _currentLayout) return;

    _currentLayout = desired;
    RebuildLayout();
}

15. History Tracker (Rolling Data Window)

For sparklines and trend graphs:

internal sealed class HistoryTracker
{
    private readonly List<double> _data = new();
    private readonly int _maxPoints;

    public HistoryTracker(int maxPoints = 50) => _maxPoints = maxPoints;
    public List<double> Data => _data;

    public void Add(double value)
    {
        _data.Add(value);
        while (_data.Count > _maxPoints)
            _data.RemoveAt(0);
    }
}

// Usage with sparkline
var cpuHistory = new HistoryTracker(50);

// In update loop
cpuHistory.Add(snapshot.CpuPercent);
sparkline.SetDataPoints(cpuHistory.Data);

16. Portal (Floating Popup)

For dropdowns, context menus, and floating panels anchored to a control:

internal class MyPortal : PortalContentContainer
{
    public MyPortal(int anchorX, int anchorY, int windowWidth, int windowHeight)
    {
        BackgroundColor = new Color(30, 30, 40);
        BorderStyle = BoxChars.Rounded;
        DismissOnOutsideClick = true;

        // Add content
        AddChild(headerLabel);
        AddChild(contentPanel);

        // Smart positioning — stays within window bounds
        var pos = PortalPositioner.CalculateFromPoint(
            new Point(anchorX, anchorY),
            new Size(popupWidth, popupHeight),
            new Rectangle(1, 1, windowWidth - 2, windowHeight - 2),
            PortalPlacement.AboveOrBelow,
            new Size(16, 3));
        PortalBounds = pos.Bounds;
    }
}

// Show portal
var portal = new MyPortal(x, y, window.Width, window.Height);
portal.Container = window;
var portalNode = window.CreatePortal(anchorControl, portal);
portal.Dismissed += (_, _) => DismissPortal();

// Dismiss portal
private void DismissPortal()
{
    if (portalNode != null)
    {
        window.RemovePortal(anchorControl, portalNode);
        portalNode = null;
    }
}

17. Notifications

Two systems — see NOTIFICATIONS.md for the full guide.

Toasts — non-blocking, single-line, auto-stacking corner overlays:

// Auto-dismisses; click to dismiss early
ws.ToastService.Show("Saved successfully", NotificationSeverity.Success);
ws.ToastService.Show("Sync started", NotificationSeverity.Info);

// Sticky (stays until clicked or dismissed) + custom position
ws.ToastService.Show("Connection lost", NotificationSeverity.Danger,
    new ToastOptions(Sticky: true, Position: ToastPosition.TopRight));

ws.ToastService.DismissAll();

NotificationStateService — title + message, can block the UI:

// Modal: blocks UI until dismissed
ws.NotificationStateService.ShowNotification(
    "Confirm", "This action cannot be undone.",
    NotificationSeverity.Warning, blockUi: true, timeout: null);

ws.NotificationStateService.DismissAllNotifications();

18. Table with Interactive Features

var table = Controls.Table()
    .WithTitle("Data Grid")
    .AddColumn("ID", TextJustification.Right, 8)
    .AddColumn("Name")
    .AddColumn("Status", TextJustification.Center, 12)
    .Interactive()
    .WithSorting()
    .WithFiltering()
    .WithFuzzyFilter()
    .WithInlineEditing()
    .WithCellNavigation()
    .WithColumnResize()
    .Rounded()
    .ShowRowSeparators()
    .WithHeaderColors(Color.White, Color.DarkBlue)
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithHorizontalAlignment(HorizontalAlignment.Stretch)
    .OnSelectedRowChanged((_, rowIdx) => ShowRowDetails(rowIdx))
    .OnRowActivated((_, rowIdx) => OpenRow(rowIdx))
    .OnCellEditCompleted((_, e) => SaveCell(e.Row, e.Column, e.NewValue))
    .Build();

// Add rows
table.AddRow("1", "Alice", "[green]Active[/]");
table.AddRow("2", "Bob", "[yellow]Pending[/]");

19. NavigationView (Sidebar + Content)

var nav = Controls.NavigationView()
    .WithNavWidth(30)
    .WithPaneHeader("[bold white]  My App[/]")
    .WithContentBorder(BorderStyle.Rounded)
    .WithContentBorderColor(Color.Grey37)
    .WithContentBackground(new Color(30, 30, 40))
    .AddHeader("Section A", Color.Cyan1, header => header
        .AddItem("Item 1", subtitle: "Description",
            content: panel => {
                panel.AddControl(Controls.Markup()
                    .AddLine("[bold]Item 1 Details[/]")
                    .Build());
            })
        .AddItem("Item 2", subtitle: "Description",
            content: panel => BuildItem2Content(panel)))
    .AddHeader("Section B", Color.Green, header => header
        .AddItem("Item 3", content: panel => BuildItem3Content(panel)))
    .Fill()
    .Build();

nav.ItemInvoked += (_, args) =>
{
    if (args.NewItem != null)
        HandleNavigation(args.NewItem.Text);
};

20. Color Theming

Centralize colors in a constants class:

public static class AppColors
{
    // Backgrounds
    public static readonly Color BaseBg = new(0x0d, 0x11, 0x17);
    public static readonly Color PanelBg = new(15, 20, 30, 200);
    public static readonly Color CardBg = new(20, 28, 40, 180);

    // Semantic colors
    public static readonly Color Accent = Color.Cyan1;
    public static readonly Color Success = new(0x4e, 0xcd, 0xc4);
    public static readonly Color Warning = new(0xff, 0xd9, 0x3d);
    public static readonly Color Danger = new(0xff, 0x6b, 0x6b);

    // Gradients (for bar graphs and sparklines)
    public static readonly Color[] HealthGradient = {
        new(0x4e, 0xcd, 0xc4),  // teal (good)
        new(0xff, 0xd9, 0x3d),  // yellow (warning)
        new(0xff, 0x6b, 0x6b)   // red (critical)
    };

    // Threshold-based color selection
    public static string ThresholdColor(double value) => value switch
    {
        < 60 => FormatHex(Success),
        < 85 => FormatHex(Warning),
        _    => FormatHex(Danger)
    };

    private static string FormatHex(Color c) => $"#{c.R:x2}{c.G:x2}{c.B:x2}";
}

// Usage in markup
var cpuColor = AppColors.ThresholdColor(cpuPercent);
markup.SetContent(new List<string> { $"[{cpuColor}]CPU: {cpuPercent:F1}%[/]" });

21. Event Handler Cleanup

Always unsubscribe event handlers to prevent memory leaks, especially in modals:

private EventHandler<string>? _inputChangedHandler;
private EventHandler<int>? _selectionHandler;

protected override void BuildContent()
{
    _inputChangedHandler = (_, text) => OnSearchChanged(text);
    searchInput.InputChanged += _inputChangedHandler;

    _selectionHandler = (_, idx) => OnSelectionChanged(idx);
    list.SelectedIndexChanged += _selectionHandler;
}

protected override void OnCleanup()
{
    if (searchInput != null && _inputChangedHandler != null)
        searchInput.InputChanged -= _inputChangedHandler;

    if (list != null && _selectionHandler != null)
        list.SelectedIndexChanged -= _selectionHandler;

    _searchCts?.Cancel();
    _searchCts?.Dispose();
}

22. Control Discovery by Name

Name controls for later lookup in async threads:

// When building
var sparkline = new SparklineBuilder()
    .WithName("cpuSparkline")  // Named for lookup
    .Build();

// In async update loop
window.FindControl<SparklineControl>("cpuSparkline")?.SetDataPoints(data);
window.FindControl<BarGraphControl>("cpuBar")?.SetValue(value);
window.FindControl<MarkupControl>("status")?.SetContent(lines);

23. Multi-View Navigation

public enum ViewState { Projects, Packages }

public class NavigationController
{
    private ViewState _currentViewState = ViewState.Projects;

    public void NavigateForward(ProjectInfo project)
    {
        _currentViewState = ViewState.Packages;
        UpdateHeaders();
        PopulatePackagesList(project.Packages);
    }

    public void NavigateBack()
    {
        if (_currentViewState == ViewState.Packages)
        {
            _currentViewState = ViewState.Projects;
            CancelPendingLoads();
            UpdateHeaders();
            PopulateProjectsList();
        }
    }

    public void HandleEnterKey() { /* navigate forward */ }
    public void HandleEscapeKey() { /* navigate back */ }
}