Skip to content

XAML Views

hifihedgehog edited this page Mar 14, 2026 · 36 revisions

XAML Views Reference

All views live in PadForge.App/Views/ under the PadForge.Views namespace. The app uses ModernWpfUI for Windows 10/11 Fluent Design styling.


Application Shell (MainWindow)

Files: MainWindow.xaml, MainWindow.xaml.cs

The application shell. Contains the NavigationView sidebar, page content area, status bar, and driver overlay.

XAML Structure

Two-row Grid:

  • Row 0 (star): NavigationView containing all page containers.
  • Row 1 (auto): Status bar Border.
  • Overlay (ZIndex=1000): Driver operation overlay (blocks UI during install/uninstall).
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>    <!-- NavigationView -->
        <RowDefinition Height="Auto"/> <!-- Status bar -->
    </Grid.RowDefinitions>

    <ui:NavigationView x:Name="NavView" PaneDisplayMode="Left" PaneTitle="PadForge" OpenPaneLength="207"
                       IsBackButtonVisible="Collapsed" IsSettingsVisible="True">
        <Grid>
            <views:DashboardPage x:Name="DashboardPageView" Visibility="Visible"/>
            <views:PadPage x:Name="PadPageView" Visibility="Collapsed"/>
            <views:DevicesPage x:Name="DevicesPageView" Visibility="Collapsed"/>
            <views:SettingsPage x:Name="SettingsPageView" Visibility="Collapsed"/>
            <views:ProfilesPage x:Name="ProfilesPageView" Visibility="Collapsed"/>
            <views:AboutPage x:Name="AboutPageView" Visibility="Collapsed"/>
        </Grid>
    </ui:NavigationView>

    <Border Grid.Row="1"> <!-- Status bar --> </Border>
    <Grid x:Name="DriverOverlay" Grid.RowSpan="2" Panel.ZIndex="1000"> <!-- ... --> </Grid>
</Grid>

Navigation Model

PadForge does not use WPF Frame-based navigation. All pages are instantiated once in the XAML Grid and visibility-swapped:

  1. NavView_SelectionChanged reads the selected item's Tag string.
  2. All page containers set to Visibility.Collapsed.
  3. The matching page set to Visibility.Visible.
  4. For controller slots (tag "Pad:{index}"), the PadPage's DataContext is set to the corresponding PadViewModel.

This approach preserves control state (scroll position, selected tabs, text field contents) across navigation since pages are never destroyed.

Sidebar Construction

The sidebar is built programmatically in BuildNavigationItems(). Fixed items:

Tag Content Icon
Dashboard Dashboard Home SymbolIcon
Devices Devices Gamepad SymbolIcon
Profiles Profiles People SymbolIcon
About About Help SymbolIcon (footer)
Settings (built-in) Built-in gear icon

Dynamic controller cards are inserted between "Devices" and "Profiles" via RebuildControllerSection(). Each card is a NavigationViewItem with custom Content containing:

  • Power/type glyph (Xbox/DS4/vJoy icon)
  • Slot label ("Controller 1", etc.)
  • Device name subtitle
  • Delete button (visible on hover)

RebuildControllerSection() is called when slots are created, deleted, or reordered. It uses a _rebuildingControllerSection guard to prevent re-entrancy during NavigationView selection changes.

Sidebar Drag Reordering

Users can drag controller cards to reorder virtual controller slots:

  1. OnCardDragStartPreviewMouseLeftButtonDown records start position.
  2. OnNavViewDragMovePreviewMouseMove checks distance threshold, then BeginCardDrag() creates a CardDragAdorner (ghost preview) and InsertionLineAdorner (drop indicator line).
  3. UpdateDragPosition — Updates adorner positions, computes target insertion index.
  4. EndCardDragPreviewMouseLeftButtonUp completes the swap. Calls SettingsManager.SwapSlots() and EnsureTypeGroupOrder().

Cross-Panel Device Drag-Drop

Devices can be dragged from the Devices page card list to a sidebar controller card:

  • DevicesPage initiates DragDrop.DoDragDrop() with DeviceRowViewModel as the data payload.
  • Sidebar NavigationViewItem handlers (DragOver, Drop) accept the drop and assign the device to that slot.

Add Controller Popup

A Popup with buttons for Xbox 360, DualShock 4, vJoy, and MIDI. Per-type buttons disabled at capacity:

  • Opacity 0.35 and "(max N)" tooltip when limit reached.
  • HasAnyControllerTypeCapacity() counts per-type from Pads[].OutputType.
  • All types share the global limit: MaxXbox360Slots = MaxDS4Slots = MaxVJoySlots = MaxMidiSlots = 16.

Status Bar

Bottom Border with four columns:

  1. Status textStatusText binding, trimmed with CharacterEllipsis.
  2. Device countConnectedDeviceCount with "device(s)" suffix.
  3. Polling frequencyPollingFrequency formatted as "{0:F0} Hz".
  4. Engine indicator — Green/gray dot (BoolToColorConverter) + EngineStatusText.

Driver Overlay

Semi-transparent overlay shown during driver install/uninstall operations:

  • ProgressRing spinner + text message.
  • Blocks all UI interaction (ZIndex=1000, Grid.RowSpan="2").
  • Shown/hidden by RunDriverOperationAsync().

Composition Root (Code-Behind)

MainWindow.xaml.cs is the service wiring hub (~2600 lines). Constructor:

  1. Creates MainViewModel as root ViewModel; sets DataContext.
  2. Sets child DataContext: Dashboard, Devices, Settings, Profiles pages.
  3. Creates services: SettingsService, InputService, RecorderService, DeviceService.
  4. Wires all ViewModel events to services:
    • StartEngineRequested/StopEngineRequested to InputService.Start()/Stop().
    • SaveRequested/ReloadRequested/ResetRequested to SettingsService.
    • Driver install/uninstall commands to DriverInstaller methods wrapped in RunDriverOperationAsync.
    • TestRumbleRequested/TestLeftMotorRequested/TestRightMotorRequested per pad.
    • Recording flow events per pad/mapping row.
    • Profile management events (New, SaveAs, Edit, Load, Delete, RevertToDefault).
    • Device assignment events via DeviceService.

Timer Architecture

Timer Interval Purpose
DispatcherTimer 33ms (~30Hz) Calls InputService.UpdateUI() to push engine state into ViewModels
_driverStatusTimer 5s Polls ViGEm/vJoy driver status for hot-plug detection
CompositionTarget.Rendering ~60fps Used by 3D/2D/Schematic views for per-frame visual updates

DashboardPage

Files: DashboardPage.xaml, DashboardPage.xaml.cs

Overview page with engine toggle, slot summary cards, DSU settings, and driver status.

Layout

  • Engine toggle: Start/Stop button with status indicator dot.
  • Slot cards: WrapPanel inside ItemsControl bound to DashboardViewModel.SlotSummaries. Each card shows slot label, device name, status dot (green=active, gray=idle), and output type icon with instance number.
  • "Add Controller" card: Shown when ShowAddController is true. Opens the type selection popup.
  • DSU settings: Enable toggle, port number field, server status text.
  • Driver status: Three rows showing ViGEmBus, HidHide, and vJoy installation status with green/gray indicator dots.
  • Card drag reordering: Slot cards support drag to reorder (same adorner system as sidebar).

PadPage

Files: PadPage.xaml, PadPage.xaml.cs

Configuration page for a single virtual controller slot. Contains a custom tab strip and 6 tab panels.

Custom Tab Strip

Horizontal bar of RadioButton controls styled via a custom ControlTemplate (the TabStripButton style). Each RadioButton:

  • Uses GroupName="PadTab" for mutual exclusion.
  • Stores the tab index in its Tag property.
  • Click handler calls vm.SelectedConfigTab = idx.

A hidden TabControl header via custom ControlTemplate provides the actual content switching. The TabControl's SelectedIndex is bound to SelectedConfigTab.

Tab Panels

Index Tab Name Description
0 Controller 3D/2D/Schematic/KBM model visualization
1 Macros Macro editor with trigger and action configuration
2 Mappings DataGrid of MappingItem rows with Record/Clear buttons
3 Sticks Dynamic ItemsControl of StickConfigItem with dead zone shape, sensitivity curves, and live preview
4 Triggers Dynamic ItemsControl of TriggerConfigItem with sensitivity curve and RangeSlider for dead zone/max range
5 Force Feedback Motor strength sliders, swap toggle, gain control, test buttons

Sticks Tab Details

Each stick section includes, in order:

  • Dead Zone Shape ComboBox (ScaledRadial, Radial, Axial, Hybrid, SlopedScaledAxial, SlopedAxial)
  • Center Offset sliders for X and Y with Calibrate Center button
  • Dead Zone X / Y sliders (0-100%)
  • Anti-Dead Zone X / Y sliders (0-100%)
  • Linear slider (0-100%)
  • Sensitivity X / Y ComboBox (preset dropdown) + CurveEditor per axis
  • Max Range X / Y sliders (1-100%)
  • Per-row reset buttons and a Reset All button at the top

Triggers Tab Details

Each trigger section includes:

  • Dead Zone / Max Range via RangeSlider (combined visualization)
  • Anti-Dead Zone slider (0-100%)
  • Sensitivity Curve ComboBox (preset dropdown) + CurveEditor
  • Per-row reset buttons and a Reset All button at the top

Controller Tab (View Switching)

Hosts one of four views depending on output type and user settings:

  1. ControllerModelView (3D, HelixToolkit) — for Xbox 360/DS4 when Use2DControllerView is false.
  2. ControllerModel2DView (2D overlays) — for Xbox 360/DS4 when Use2DControllerView is true.
  3. ControllerSchematicView (procedural) — for custom vJoy (non-gamepad preset), always.
  4. KBMPreviewView — for Keyboard+Mouse output type, always (see below).
  5. MidiPreviewView — for MIDI output type, always.

ApplyViewMode() in PadPage.xaml.cs controls which view is visible:

  • Custom vJoy: Schematic always, 2D/3D toggle hidden.
  • Gamepad: 2D/3D toggle visible, user preference from SettingsViewModel.Use2DControllerView.

BindActiveModelView() wires the active view:

  1. Unbinds all three views.
  2. Subscribes the active view's ControllerElementRecordRequested event.
  3. Calls Bind(vm) on the active view.

All three views fire ControllerElementRecordRequested with a PadSetting target name string for click-to-record.

View Mode Toggle

Button with icon that switches between 2D and 3D:

  • E8B9 (flat/photo icon) shown in 3D mode, click switches to 2D.
  • F158 (3D/cube icon) shown in 2D mode, click switches to 3D.
  • Hidden when displaying custom vJoy schematic.

vJoy Config Bar

Visible when OutputType == VJoy. Contains:

  • Preset ComboBox (Xbox 360, DualShock 4, Custom).
  • Four spinners visible only in Custom: ThumbstickCount, TriggerCount, PovCount, ButtonCount.
  • VJoyPresetCombo_SelectionChanged updates VJoyConfig.Preset and calls SyncVJoyCustomFields().
  • _syncingVJoyConfig guard prevents recursive updates during programmatic sync.

MIDI Config Bar

Visible when OutputType == Midi. Contains:

  • Channel ComboBox (1-16).
  • CC Count and Start CC spinners (interdependent clamping to 0-127 range).
  • Note Count and Start Note spinners (interdependent clamping to 0-127 range).
  • Velocity spinner (0-127).

All fields bind to PadViewModel.MidiConfig (MidiSlotConfig) properties.

Multi-Device Selector

ComboBox bound to MappedDevices with SelectedMappedDevice. Visible when multiple physical devices are assigned to one slot.

Copy From Dialog

"Copy From" button opens CopyFromDialog to copy mappings from another slot's device.


DevicesPage

Files: DevicesPage.xaml, DevicesPage.xaml.cs

Lists all detected input devices with raw input state visualization.

Device List (Left Panel)

ListBox with card ItemTemplate. Each card shows:

  • Status dot (green=online, gray=offline via BoolToColorConverter)
  • Device name (bold)
  • Slot badges (SlotBadges collection with sequential numbering)
  • Device type and capabilities summary
  • VID/PID

Selection highlighting: Custom ControlTemplate for ListBoxItem with 4px accent-colored left bar on the selected card.

Detail Panel (Right Panel)

Shown when a device is selected (HasSelectedDevice). Contains:

  • Axes: ItemsControl of ProgressBar elements (0-100% range via AxisToPercentConverter). Each bar shows name, normalized bar, and raw value.
  • Buttons: WrapPanel of circles. Accent fill when pressed (BoolToOpacityConverter). For keyboard devices, displays KeyboardKeys on a positioned Canvas inside a Viewbox (QWERTY layout).
  • POV hats: ItemsControl bound to RawPovs. Each POV displays as a compass with a RotateTransform arrow (PovToAngleConverter converts centidegrees to degrees).
  • Gyroscope/Accelerometer: Grid showing X/Y/Z values when HasGyroData/HasAccelData are true.

Slot Toggle Buttons

ItemsControl of toggle buttons bound to ActiveSlotItems. Accent fill when IsAssigned. Click fires ToggleSlotCommand to assign/unassign the device to a controller slot.

Device Drag to Sidebar

Device cards support drag start via PreviewMouseLeftButtonDown + PreviewMouseMove. Dropping on a sidebar controller card assigns the device to that slot.

Input Hiding

The device detail panel includes per-device input hiding controls:

  • Hide from games (HidHide) toggle — hides the physical device using the HidHide driver
  • Consume mapped inputs toggle — suppresses keyboard/mouse inputs via low-level hooks
  • Input Mode section (gamepad devices only) — Force raw joystick mode toggle

SettingsPage

Files: SettingsPage.xaml, SettingsPage.xaml.cs

Application settings organized in vertical card sections using CardBorder style.

Sections

Section Icon Contents
Appearance E790 Theme ComboBox (System Default / Light / Dark)
Input Engine E9F5 Auto-start toggle, polling on focus loss, polling interval NumberBox (1-16ms)
Window E737 Minimize to tray, start minimized, start at login checkboxes
HidHide Driver ED1A Status dot, version text, Install/Uninstall buttons, whitelist ListBox with Add/Remove buttons
ViGEmBus Driver E7FC Status dot, version text, Install/Uninstall buttons
vJoy Driver joystick SVG Status dot, version text, Install/Uninstall buttons
Settings File E8A5 File path, Save/Reload/Reset/Open Folder buttons, unsaved indicator
Diagnostics E9D9 App version, .NET runtime, SDL version

Driver sections use BoolToColorConverter for status dots and BoolToVisibilityConverter (with Invert parameter) to toggle Install/Uninstall button visibility.

Web Controller

The Settings page includes web controller configuration:

  • Enable web controller checkbox bound to EnableWebController
  • Port text field bound to WebControllerPort

ProfilesPage

Files: ProfilesPage.xaml, ProfilesPage.xaml.cs

Per-app profile management.

Layout

  • Auto-switch toggle: CheckBox bound to EnableAutoProfileSwitching.
  • Active profile info: TextBlock showing current profile name.
  • Profile ListBox: Each item shows profile name, executable list (trimmed), and topology badges (Xbox/DS4/vJoy/MIDI counts with mini SVG icons). Badges use DataTrigger on count = 0 to collapse empty types. "No slots" fallback badge shown when all counts are zero.
  • Action buttons: New, Save As, Edit, Load, Delete.

Profile ListBox Item Template

Each profile item contains:

  1. Profile name (FontWeight="SemiBold").
  2. Executable list (TextTrimming, collapsed when empty via DataTrigger).
  3. Type count badges in a StackPanel:
    • Xbox badge: Xbox SVG icon + count, collapsed when XboxCount == 0.
    • DS4 badge: PlayStation SVG icon + count, collapsed when DS4Count == 0.
    • vJoy badge: Joystick SVG icon + count, collapsed when VJoyCount == 0.
    • MIDI badge: Music note SVG icon + count, collapsed when MidiCount == 0.
    • "No slots" fallback when TopologyLabel == "No slots".

AboutPage

Files: AboutPage.xaml, AboutPage.xaml.cs

Static information page showing application name, version, copyright, license information, and links.


Dialog Windows

CopyFromDialog

Files: CopyFromDialog.xaml, CopyFromDialog.xaml.cs

Modal dialog for copying mappings from another slot's device. Lists all slots with their mapped devices for selection.

ProfileDialog

Files: ProfileDialog.xaml, ProfileDialog.xaml.cs

Modal dialog for creating/editing profiles. Fields: profile name, executable list (comma-separated).


Value Converters

All converters live in PadForge.App/Converter/ under the PadForge.Converters namespace and are registered as StaticResource in App.xaml.

Converter Key Input Output Description
AxisToPercentConverter AxisToPercentConverter ushort (0-65535) double (0-100) or string ("50.0%") Axis value to percentage. Returns formatted string when target type is string.
BoolToColorConverter BoolToColorConverter bool SolidColorBrush true = Green #4CAF50, false = Gray #9E9E9E. Brushes are frozen.
BoolToInstallTextConverter BoolToInstallTextConverter bool string Default: "Installed"/"Not Installed". With parameter "Action": "Uninstall"/"Install".
BoolToOpacityConverter BoolToOpacityConverter bool double true = 1.0, false = 0.2. Parameter: "falseVal" or "falseVal,trueVal".
BoolToVisibilityConverter BoolToVisibilityConverter bool Visibility true = Visible, false = Collapsed. Parameter "Invert" reverses. Supports ConvertBack.
NormToCanvasConverter NormToCanvasConverter double (0-1) double Canvas position. Parameter: "canvasDim" or "canvasDim,dotSize". Dot default 14px.
NormToTriggerHeightConverter NormToTriggerHeightConverter double (0-1) double Pixel height. Parameter = max size (default 40). Used for trigger bars and motor indicators.
NormToTriggerSlideConverter NormToTriggerSlideConverter double (0-1) double Canvas.Top for sliding bar. Parameter: "containerH,barH". 0 = bottom, 1 = top.
NullToCollapsedConverter NullToCollapsedConverter object Visibility Non-null = Visible, null = Collapsed.
PercentToSizeConverter PercentToSizeConverter int/double (0-100) double Pixel size from percentage. Parameter = max size. Used for dead zone ring visualization.
PovToAngleConverter PovToAngleConverter int (centidegrees) double (degrees) 0-35999 centidegrees to 0-359.99 degrees. Returns NaN for -1 (centered).
StatusToColorConverter StatusToColorConverter string SolidColorBrush "Running"/"Online"/"Connected" = Green, "Stopped"/"Offline"/"Error" = Red, "Warning" = Orange, other = Gray.
StringToGeometryConverter StringToGeometry string Geometry Parses SVG path data string to Geometry via Geometry.Parse().
StringToVisibilityConverter StringToVisibility string Visibility Non-null and non-empty = Visible, else Collapsed.

Resource Dictionaries and Theming

App.xaml Resource Hierarchy

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ui:ThemeResources>           <!-- ModernWPF theme (Light/Dark/HighContrast) -->
                <ui:ThemeResources.ThemeDictionaries>
                    <!-- Per-theme brush overrides: RangeSliderThumbFill, PopupBackgroundBrush -->
                </ui:ThemeResources.ThemeDictionaries>
            </ui:ThemeResources>
            <ui:XamlControlsResources />  <!-- ModernWPF control styles -->
            <ResourceDictionary Source="/Resources/ControllerIcons.xaml"/>
        </ResourceDictionary.MergedDictionaries>

        <!-- 14 global converter registrations -->
    </ResourceDictionary>
</Application.Resources>

ControllerIcons.xaml

File: PadForge.App/Resources/ControllerIcons.xaml

Contains:

DrawingImage icons (used in sidebar, dashboard, profiles):

Key Source Description
XboxControllerIcon svgrepo.com (32x32) Xbox logo icon
DS4ControllerIcon svgrepo.com (32x32) PlayStation logo icon
VJoyControllerIcon svgrepo.com (24x24) Joystick icon
MidiControllerIcon svgrepo.com MIDI / music note icon
GenericControllerIcon svgrepo.com (512x512, scaled) Generic gamepad with D-pad and face buttons

All icons use DynamicResource SystemControlForegroundBaseHighBrush as fill for automatic theme adaptation.

Shared card styles:

Key TargetType Properties
CardBorder Border ChromeMediumLow background, 8px corner radius, 16px padding, 12px bottom margin
CardTitle TextBlock 14px font, SemiBold weight
CardDescription TextBlock 12px font, 0.6 opacity, wrap enabled

Theme Switching

SettingsViewModel.SelectedThemeIndex maps to:

  • 0 = System Default (follows Windows setting)
  • 1 = Light
  • 2 = Dark

Applied via ThemeManager.Current.ApplicationTheme in OnThemeChanged handler.

Custom Per-Theme Brushes

Defined in ThemeDictionaries within App.xaml:

Brush Key Light Dark Usage
RangeSliderThumbFill #F0F0F0 #F0F0F0 RangeSlider custom thumb fill
PopupBackgroundBrush #FFE0E0E0 #FF3A3A3A Add controller popup background

Common XAML Patterns

Card-Based Layout

Settings and driver pages use a consistent card pattern:

<Border Style="{StaticResource CardBorder}">
    <StackPanel>
        <StackPanel Orientation="Horizontal" Margin="0,0,0,4">
            <TextBlock Text="&#xE790;" FontFamily="Segoe MDL2 Assets" FontSize="16"
                       VerticalAlignment="Center" Margin="0,0,8,0"/>
            <TextBlock Text="Card Title" Style="{StaticResource CardTitle}"/>
        </StackPanel>
        <TextBlock Text="Description text." Style="{StaticResource CardDescription}"/>
        <!-- Card content -->
    </StackPanel>
</Border>

Status Indicator Dots

Used throughout for online/offline and installed/not-installed states:

<Ellipse Width="8" Height="8"
         Fill="{Binding SomeBool, Converter={StaticResource BoolToColorConverter}}"
         VerticalAlignment="Center" Margin="0,0,8,0"/>

Segoe MDL2 Asset Icons

The app uses Segoe MDL2 Assets font for icon glyphs rather than image resources:

<TextBlock Text="&#xE713;" FontFamily="Segoe MDL2 Assets" FontSize="20"/>

Common codes used: E713 (settings gear), E790 (personalization), E9F5 (processing), E737 (star), ED1A (shield), E7FC (gamepad), E8A5 (save), E9D9 (bug), E8F1 (group), E8B9 (photo), F158 (3D).

ModernWPF NumberBox

Used for numeric input with inline spin buttons:

<ui:NumberBox Value="{Binding PollingRateMs, Mode=TwoWay}"
              Minimum="1" Maximum="16"
              SpinButtonPlacementMode="Inline" Width="120"/>

Code-Behind Patterns

Bind/Unbind Pattern

All three visualization views (3D, 2D, Schematic) follow the same interface:

public void Bind(PadViewModel vm)    // Subscribe to PropertyChanged, hook rendering, load model
public void Unbind()                 // Stop flash, unhook rendering, clear VM reference

This pattern allows PadPage to switch between views cleanly when the output type or user preference changes.

CompositionTarget.Rendering with Dirty Flag

Views use CompositionTarget.Rendering for per-frame visual updates, gated by a _dirty flag:

private bool _dirty;

private void OnRendering(object sender, EventArgs e)
{
    if (!_dirty || _vm == null) return;
    _dirty = false;
    // Update visuals...
}

private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    _dirty = true;  // Coalesce multiple property changes into one render frame
}

This batches multiple ViewModel property changes (button presses, axis movements) into a single visual update per render frame, avoiding redundant work.

DispatcherTimer Flash Animation

Used for "Map All" recording flow across all three views:

private DispatcherTimer _flashTimer;
private string _flashTarget;
private bool _flashOn;

private void UpdateFlashTarget(string target)
{
    // Start or stop flash timer based on target
    // Timer callback toggles highlight/default materials at 400ms (2D/3D) or 170ms (Schematic)
}

Clone this wiki locally