CodeWF.AvaloniaControls Adds Guide Control: From AtomUI Tour to Vex Implementation

CodeWF.AvaloniaControls Adds Guide Control: From AtomUI Tour to Vex Implementation

Introduces the new Guide control in CodeWF.AvaloniaControls, highlighting mask highlighting, popup positioning, dynamic menu MenuItem guidance, non-modal prompts, TabItem switching, and its implementation in Vex's first-launch guide.

Last updated 5/24/2026 3:39 PM
dotnet9
10 min read
Category
.NET Avalonia Desktop Development
Topic
Avalonia
Tags
C# Avalonia CodeWF Guide Vex Desktop Application

CodeWF.AvaloniaControls adds a new Guide control for onboarding, feature walkthroughs, and targeted tooltips in Avalonia desktop applications.

It draws inspiration from AtomUI's Tour concept but focuses on desktop-specific scenarios: menus, submenus, popups, TabItem, delayed target appearance, and window resize — all must be reliably positioned.

First, see it in action within Vex. The guide appears automatically on first launch and can be reopened later via the Help menu:

The control library demo also includes two smaller examples: a basic multi-step guide with cover content, custom buttons, and a non-modal hint with text progress.

What Does Guide Solve?

Highlighting a regular page button is easy; the real challenge is dynamic entry points in desktop applications.

Guide currently covers these scenarios:

  • Multi-step guide: previous, next, finish, close.
  • Each step can target a different control, or have no target for center-aligned explanations.
  • Mask cutout, highlight rounded corners, target margins, and card direction control.
  • Auto-scroll targets into view when inside a scrollable region.
  • Retry with a delay if the target appears later.
  • Locate targets inside Menu, Popup, Flyout, and other popup layers.
  • Execute commands or events before entering a step—useful for expanding menus or switching tabs.
  • Refresh highlight position after layout changes or window resize.

Source location:

https://github.com/dotnet9/CodeWF.AvaloniaControls/tree/main/src/CodeWF.AvaloniaControls/Controls/Guide
https://github.com/dotnet9/CodeWF.AvaloniaControls/tree/main/src/CodeWF.AvaloniaControls.Themes/Themes/Controls/Guide.axaml

Control Structure

A small set of core types:

  • Guide: The main control that manages opening, closing, current step, popup, and target resolution.
  • GuideStep: Declarative step in XAML.
  • GuideStepOption / IGuideStepOption: Used when creating steps in code.
  • GuideOverlay: Draws the mask and highlight hole.
  • DefaultGuideIndicator / TextGuideIndicator: Dot or text progress.
  • GuidePlacementMode: Card position.
  • GuideMissingTargetBehavior: Centers, skips, or closes when target is missing.

The template contains three key popups:

  • PART_MaskPopup: Full-window mask.
  • PART_TargetMaskPopup: Used when the target is inside another popup host.
  • PART_Popup: The guide card.

This structure means the business side only declares "which controls to guide"; the mask, positioning, buttons, indicator, and cleanup are handled internally.

Basic Usage

Place the Guide control in the root layout of your page and assign each GuideStep its target control:

<Grid>
    <StackPanel Orientation="Horizontal" Spacing="10">
        <Button x:Name="UploadButton" Content="Upload File" />
        <Button x:Name="SaveButton" Content="Save Changes" />
        <Button x:Name="MoreButton" Content="More Actions" />
    </StackPanel>

    <codewf:Guide x:Name="BasicGuide" Placement="Bottom" PopupOffset="14">
        <codewf:GuideStep
            Target="{Binding ElementName=UploadButton}"
            Title="Upload File"
            Description="Add local files to the processing queue." />
        <codewf:GuideStep
            Target="{Binding ElementName=SaveButton}"
            Placement="Right"
            Title="Save Changes"
            Description="Save the current workspace." />
        <codewf:GuideStep
            Target="{Binding ElementName=MoreButton}"
            Placement="Top"
            Title="More Actions"
            Description="Continue to expand export, copy, or batch processing." />
    </codewf:Guide>
</Grid>

Open the guide:

BasicGuide.GoTo(0);
BasicGuide.Show();

For a non-modal hint, simply hide the mask; you can also switch to a text indicator:

<codewf:Guide
    x:Name="NonMaskGuide"
    IsShowMask="False"
    Placement="Top"
    StyleType="Primary">
    <codewf:Guide.Indicator>
        <codewf:TextGuideIndicator />
    </codewf:Guide.Indicator>
</codewf:Guide>

Individual steps can also adjust the highlight area:

<codewf:GuideStep
    Target="{Binding ElementName=PreviewPanel}"
    Placement="Left"
    GapOffsetX="16"
    GapOffsetY="16"
    GapRadius="14"
    Title="Custom Highlight Area"
    Description="Widen the selection margins and rounded corners to highlight the entire block." />

Mask and Positioning

GuideOverlay creates a hole using the EvenOdd geometry rule: first draw a full-screen rectangle, then add the target area as a second rectangle to the same GeometryGroup with FillRule.EvenOdd — the target area remains transparent.

Target coordinates are not directly dependent on TranslatePoint. Instead, we get screen coordinates first, then convert them back to the client coordinates of the corresponding TopLevel:

var targetTopLeft = target.PointToScreen(new Point(0, 0));
var origin = relativeTopLevel.PointToClient(targetTopLeft);
var rect = new Rect(origin, target.Bounds.Size);
var result = rect.Inflate(new Thickness(gapX, gapY));

This approach ensures compatibility with targets inside menus, popups, Flyouts, or other popup hosts.

Dynamic Menu Guidance

Menu item guidance is the most important enhancement in this release. The following GIF shows only the menu steps: File menu, Open Folder, Export submenu, Paragraph menu, Format menu, View menu, and Theme submenu.

The issue with menu items is that child MenuItem controls only appear in the visual tree after the parent menu is opened. The solution is to open the parent menu before entering the step, then let the Guide resolve the target with a delay.

Simplified demo code:

<Menu>
    <MenuItem x:Name="GuideThemeMenu" Header="Theme Color">
        <MenuItem x:Name="GuideThemeBlueItem" Header="Blue" />
        <MenuItem x:Name="GuideThemeGreenItem" Header="Green" />
        <MenuItem x:Name="GuideThemePurpleItem" Header="Purple" />
    </MenuItem>
</Menu>

<codewf:Guide
    x:Name="DynamicGuide"
    TargetResolveDelay="00:00:00.220"
    StepOpening="DynamicGuide_OnStepOpening">
    <codewf:GuideStep
        Target="{Binding ElementName=GuideThemeMenu}"
        Title="Theme Color Menu" />
    <codewf:GuideStep
        Target="{Binding ElementName=GuideThemeBlueItem}"
        Placement="RightBottom"
        Title="Blue Theme" />
</codewf:Guide>

Open the parent menu when entering a menu item step:

private void DynamicGuide_OnStepOpening(object? sender, GuideStepEventArgs e)
{
    GuideThemeMenu.IsSubMenuOpen = e.Index is >= 1 and <= 3;
    Dispatcher.UIThread.Post(
        () => GuideThemeMenu.IsSubMenuOpen = true,
        DispatcherPriority.Background);
}

The key is TargetResolveDelay. Menu popup creation and layout are not fully synchronous; delaying target resolution slightly improves positioning stability.

Implementation in Vex

Vex's title bar menus are in ShellTitleMenuView.axaml; key menu items are named and exposed to the main window via code-behind:

public MenuItem FileMenuTarget => FileMenuItem;
public MenuItem OpenFolderMenuTarget => OpenFolderMenuItem;
public MenuItem ExportMenuTarget => ExportMenuItem;
public MenuItem TableMenuTarget => TableMenuItem;
public MenuItem LinkMenuTarget => LinkMenuItem;
public MenuItem SourceModeMenuTarget => SourceModeMenuItem;
public MenuItem OutlineMenuTarget => OutlineMenuItem;
public MenuItem ThemeDarkMenuTarget => ThemeDarkMenuItem;
public MenuItem BeginGuideMenuTarget => BeginGuideMenuItem;

Before starting the guide, the main window assigns these controls to the corresponding steps:

private void ConfigureOnboardingGuideTargets()
{
    GuideFileMenuStep.Target = TitleMenuView.FileMenuTarget;
    GuideFileOpenStep.Target = TitleMenuView.OpenFolderMenuTarget;
    GuideFileExportStep.Target = TitleMenuView.ExportMenuTarget;
    GuideParagraphMenuStep.Target = TitleMenuView.TableMenuTarget;
    GuideFormatMenuStep.Target = TitleMenuView.LinkMenuTarget;
    GuideViewMenuStep.Target = TitleMenuView.SourceModeMenuTarget;
    GuideViewOutlineMenuStep.Target = TitleMenuView.OutlineMenuTarget;
    GuideThemeMenuStep.Target = TitleMenuView.ThemeDarkMenuTarget;
    GuideHelpMenuStep.Target = TitleMenuView.BeginGuideMenuTarget;
}

On each step transition, all menus are closed first, then the required menus for the current step are opened. For submenus like theme colors, both parent and child are opened consecutively:

case ThemeColorGuideMenu:
    ThemeMenuItem.IsSubMenuOpen = true;
    ThemeColorMenuItem.IsSubMenuOpen = true;
    break;

The entire pipeline can be summarized in five steps:

  1. GuideStep.Target points to a specific MenuItem.
  2. StepOpening opens the parent menu.
  3. TargetResolveDelay waits for the popup layout.
  4. Guide resolves the target, draws the mask, and displays the card.
  5. On step end or guide close, menus are collapsed.

TabItem Switching

Vex's left sidebar uses a TabControl. The guide needs to explain "Files" and "Outline" separately. Both steps reuse the same sidebar target, but the tab is switched before entering the step.

Both steps target the same SidebarGuideTarget:

<codewf:GuideStep
    x:Name="GuideSidebarFilesStep"
    Target="{Binding ElementName=SidebarGuideTarget}"
    Title="{i18n:I18n {x:Static l:VexL.GuideSidebarFilesTitle}}" />

<codewf:GuideStep
    x:Name="GuideSidebarOutlineStep"
    Target="{Binding ElementName=SidebarGuideTarget}"
    Title="{i18n:I18n {x:Static l:VexL.GuideSidebarOutlineTitle}}" />

When entering a step, switch the business state and post a refresh to the UI background queue:

private void PrepareOnboardingGuideStep(IGuideStepOption step)
{
    if (DataContext is not MainWindowViewModel viewModel)
    {
        return;
    }

    if (ReferenceEquals(step, GuideSidebarFilesStep))
    {
        viewModel.Layout.ShowFiles();
        QueueOnboardingGuideRefresh();
        return;
    }

    if (ReferenceEquals(step, GuideSidebarOutlineStep))
    {
        viewModel.Layout.ShowOutline();
        QueueOnboardingGuideRefresh();
    }
}

private void QueueOnboardingGuideRefresh()
{
    Dispatcher.UIThread.Post(OnboardingGuide.Refresh, DispatcherPriority.Background);
}

This step is crucial. The tab content needs to be laid out before correct dimensions are available; otherwise, the highlight area may stay at the old position.

First Launch

Vex does not show the guide on every startup. There is a configuration flag:

<add key="HasSeenOnboardingGuide" value="false" />

After the window opens, if the user has not seen the guide, mark it as seen and dispatch the guide once:

private void QueueFirstRunOnboardingGuide()
{
    if (_settingsStore is null || _settingsStore.Current.HasSeenOnboardingGuide == true)
    {
        return;
    }

    _settingsStore.Update(settings => settings with { HasSeenOnboardingGuide = true });
    Dispatcher.UIThread.Post(BeginOnboardingGuide, DispatcherPriority.Background);
}

Afterwards, the user can reopen the guide from the Help menu, which returns to the first step:

private void BeginOnboardingGuide()
{
    ConfigureOnboardingGuideTargets();
    TitleMenuView.CloseGuideMenus();
    OnboardingGuide.GoTo(0);
    OnboardingGuide.Show();
}

Known Limitations

Dynamic menu guidance has one boundary case: if the application loses focus during the guide, Avalonia's light-dismiss behavior may close the menu popup first, and subsequent guide steps may also disappear.

The current implementation prioritizes PointerPressed navigation for the previous, next, and finish buttons to avoid being preempted by menu closure when clicking guide buttons. However, when the window genuinely loses focus, the menu popup may still close according to platform rules.

Two potential future directions:

  • Strengthen popup target persistence and focus restoration inside Guide.
  • After the menu is fully expanded, capture a static snapshot and paste it over the mask layer, then guide based on snapshot positions. This approach is more stable but cannot respond to real menu item interactions.

Summary

Guide now covers common onboarding scenarios in desktop applications: basic multi-step, center-aligned explanations, cover content, custom buttons, non-modal hints, menu item guidance, submenus, TabItem switching, delayed target appearance, and first-run-only display.

After implementing this in Vex, the clearest takeaway is: desktop application onboarding cannot rely solely on static button highlighting. Real entry points are often hidden behind menus, popups, and tabs—the guide control must evolve alongside business state.

Related links:

Keep Exploring

Related Reading

More Articles
Same category / Same topic 5/18/2026

枝见 Zhijian: A Markdown Mind Map Editor Built with Avalonia

This article introduces Zhijian, a local mind map editor based on Avalonia, supporting blank creation, folder loading, precise onboarding guidance, macOS shortcut adaptation, outline/Markdown/mind map synchronization, node notes, thumbnails, zoom, canvas dragging, and Markdown/OPML/XMind file exchange.

Continue Reading
Same category / Same topic 5/16/2026

CodeWF.Markdown: A Markdown Rendering Control Based on Avalonia 12

This article introduces the repository address of CodeWF.Markdown, NuGet installation method, full package line, Lite package line, real-time editing preview, typography themes, code highlighting, image preview, mathematical formulas, multi-Viewer coverage, and incremental rendering capabilities.

Continue Reading