diff --git a/.agents/skills/wpf-visual-tree-cli/SKILL.md b/.agents/skills/wpf-visual-tree-cli/SKILL.md new file mode 100644 index 0000000..f68ba28 --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/SKILL.md @@ -0,0 +1,128 @@ +--- +name: wpf-visual-tree-cli +description: Operate WpfVisualTreeMcp through the `wpfinspect` or `WpfVisualTreeMcp.Server.exe` command-line interface to discover, attach to, inspect, diagnose, screenshot, and intentionally drive running Windows WPF applications. Use for WPF visual-tree, dependency-property, binding, DataContext, resource, style, layout, screenshot, interaction, wait, snapshot, diff, and live-property experiments from PowerShell or another shell. Choose self-hosting when it is safer or more reliable than runtime auto-injection. +--- + +# WPF Visual Tree CLI + +Use the CLI for quick diagnostics, repeatable shell automation, and environments without a configured MCP client. Keep inspection read-only unless the user explicitly requests interaction or live modification. + +## Establish the executable + +Prefer the installed .NET tool command: + +```powershell +Get-Command wpfinspect -ErrorAction Stop +wpfinspect help +``` + +If it is unavailable, check for an extracted `WpfVisualTreeMcp.Server.exe`. Install or update the global tool only when authorized: + +```powershell +dotnet tool install --global WpfVisualTreeMcp +dotnet tool update --global WpfVisualTreeMcp +``` + +Run `wpfinspect help` and `wpfinspect --help` before relying on bundled syntax when the installed version differs from the current repository. Read [references/cli-reference.md](references/cli-reference.md) for the current command map and examples. + +Distinguish command compatibility from artifact contents. The published v0.12.0 artifacts have a known Auto-injection packaging defect: they omit the native bootstrapper and the complete .NET Framework Inspector dependency closure. The current repository source builds both bootstrapper architectures, packages the dependency closure under both architecture directories, selects the Inspector payload for the target process architecture, resolves co-located .NET Framework dependencies only for the Inspector payload chain, and validates publish/pack payloads. Do not assume an installed package contains that repair until its release notes or package contents confirm it. + +Do not treat `dotnet build` alone as a complete Auto-injection source build because it skips the native `.vcxproj`. Read [references/cli-reference.md](references/cli-reference.md) for the native x64/Win32 MSBuild commands before publishing a source build for injection. + +## Decide whether to suggest an update + +Suggest `dotnet tool update --global WpfVisualTreeMcp` only when the installation is the global .NET tool, a newer stable package exists, and the observed limitation is plausibly version-specific. Check rather than assume: + +```powershell +Get-Command wpfinspect -ErrorAction SilentlyContinue +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +dotnet tool search WpfVisualTreeMcp --detail +``` + +Consult the newer release notes or changelog before claiming that an update fixes a problem. Recommend updating when a newer version adds the missing command or option, fixes the encountered error, restores packaging artifacts such as an architecture helper, or brings an older command schema in line with the current reference. + +For failed Auto-injection from v0.12.0, specifically check whether a newer stable release includes the native-bootstrapper and dependency-closure repair. If no such release exists, explain that current source contains the fix and offer a source build or self-hosted mode; do not imply that reinstalling the same package repairs it. + +Do not recommend an update for limitations that remain architectural: injection blocked by policy or privilege, the need to capture startup diagnostics, or incompatible target frameworks for self-hosting. Route those cases to self-hosted mode as described below. + +Do not execute the update merely to check availability. Explain the evidence and ask before changing a working global tool, especially when scripts may depend on a pinned version. If the package is absent, suggest `dotnet tool install --global WpfVisualTreeMcp`; if the user runs an extracted release EXE, direct them to update that release instead because the global-tool command will not replace it. + +After an approved update, verify the installed version and live syntax, then retry the smallest safe failing operation: + +```powershell +dotnet tool update --global WpfVisualTreeMcp +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +wpfinspect help +``` + +## Follow the operating workflow + +1. List candidate processes and parse stdout as JSON: + + ```powershell + $wpfProcessList = wpfinspect list --compact | ConvertFrom-Json + $wpfProcessList.processes | Format-Table processId, processName, mainWindowTitle, runtimeType, dotNetVersion + ``` + + Do not assign to `$pid`; PowerShell treats `$PID` case-insensitively as a read-only automatic variable. Use `$targetProcessId`. + +2. Select an exact process ID. Prefer PID over process name when multiple instances exist. Re-run `list` after an application restart because its PID changes. + +3. Probe attachment without injection first: + + ```powershell + $attachResult = wpfinspect attach --pid $targetProcessId --compact | ConvertFrom-Json + $attachResult.inspectorStatus + ``` + + Continue if the Inspector is already loaded. If it is not loaded, choose auto-injection or self-hosting using the rules below. Auto-inject once; the Inspector remains in that target process for later one-shot commands: + + ```powershell + wpfinspect attach --pid $targetProcessId --auto-inject + ``` + +4. Inspect from broad to narrow: `tree` or `find`, then `props`, `bindings`, `data-context`, `styles`, `layout`, `evaluate-binding`, or `explain-triggers`. Use returned `elem_...` handles only while the target process and element remain alive. + +5. Prefer `wait-for` over sleep-and-retry loops. Use `select-item` instead of clicking virtualized ComboBox/ListBox/ListView/TabControl entries. + +6. Keep stdout machine-readable. Add `--compact` for parsing and `--verbose` only when diagnostics on stderr are needed. + +## Protect target state + +Treat these commands as state-changing and run them only when the user requests the corresponding action: `click`, `select-item`, `set-text`, `send-keys`, `set-property`, `revert-property`, and `clear-binding-errors`. `highlight` visibly alters the target temporarily. + +Prefer UI Automation behavior. Use `--physical` only when necessary because it moves the real cursor, focuses or raises the window, and can affect whichever desktop is active. Double-click and right-click are physical operations. + +Before `set-property`, take a labeled snapshot when useful. Revert experimental edits after measurement unless the user asks to leave them applied. + +## Choose auto-injection or self-hosting + +Use auto-injection when the application cannot be modified, its security policy permits runtime DLL injection, and the installed artifact contains the complete injection payload. Current source builds package the x64 and x86 bootstrappers, the `net48` Inspector dependency closure with resolution scoped to the Inspector payload chain, the x86 helper, and the CoreCLR runtime configuration; inherent injection constraints still apply. + +Recommend self-hosted mode instead when it solves a concrete auto-injection limitation: + +- Security policy, endpoint protection, process hardening, or organizational rules block `CreateRemoteThread`/`LoadLibrary` injection. +- Privilege or user-session boundaries prevent the server from opening and modifying the target process. +- Cross-bitness injection fails because the architecture-matching helper or required x86 .NET 8 runtime is missing. +- Diagnostics must start with application startup so early binding errors or UI initialization behavior are not missed. +- Repeated application launches need a deterministic Inspector endpoint without re-injecting each new PID. +- Injection destabilizes this particular application or its custom CLR hosting environment. + +Self-hosting requires source changes: reference the Inspector target matching the application and call `InspectorService.Initialize(...)` during WPF startup. The current repository Inspector targets `net472`, `net48`, and `net8.0-windows`, so .NET Framework 4.7.2 applications can self-host directly without retargeting to 4.8. Match a modern WPF application to `net8.0-windows`, not plain `net8.0`. + +Do not confuse self-hosting support with the injected payload. Auto-injection into .NET Framework processes deliberately uses the `net48` Inspector and requires the .NET Framework 4.8 runtime. Adding `net472` enables source-integrated self-hosting; it does not add a `net472` Auto-injection payload. + +Do not suggest self-hosting when target source cannot be changed. Do not suggest MCP as a remedy for blocked injection; MCP uses the same Inspector deployment choices. + +## Recover from common failures + +- No processes: verify the WPF app has a visible main window, match privilege level, and run `list` again. +- Inspector not loaded: inspect `inspectorStatus`; then explicitly auto-inject or explain the self-hosted alternative. +- Target restarted or handle failed: re-list, use the new PID, attach, and reacquire element handles. +- x64 server to x86 target fails: verify the bundled x86 helper, x86 bootstrapper, and x86 .NET 8 runtime. If they are absent from v0.12.0, recommend a newer release containing the packaging repair or a current source build; prefer self-hosting if installing them is unsuitable. +- Native bootstrapper or managed dependency is missing: treat this as the known v0.12.0 packaging defect when applicable. Do not keep retrying injection; update to a release containing the repair, use a verified current source build, or self-host. +- ARM64 target: explain that native ARM64 Auto-injection is unsupported; use a supported x64/x86 target or self-host when the application architecture and Inspector reference permit it. +- Popup/menu missing from screenshot: use `screenshot --mode screen` while the window is visible and unobstructed. Use default `render` mode for covered windows and ordinary controls. +- Unscrolled ScrollViewer content is missing: when installed help lists it, pass the precise ScrollViewer or owning control handle to `screenshot --full-content`; use a larger `--max-height` for long output. This render-mode option restores the original scroll position after paging virtualized content. It cannot be combined with `--mode screen`, logically scrolling virtualized controls with horizontal overflow are unsupported, output is area-downscaled to the built-in safe limit, and captures that exceed the retained-bitmap or encoded-PNG budgets or 25-second cooperative deadline fail safely. +- Command fails opaquely: rerun that command with `--verbose`, keeping stderr separate from stdout JSON. +- Missing command, option, helper, or known fixed behavior: check installed versus current package versions and release notes; suggest a global-tool update only when the evidence connects the limitation to version drift. diff --git a/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml b/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml new file mode 100644 index 0000000..fac48cd --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "WPF Visual Tree CLI" + short_description: "Inspect and drive WPF apps from the CLI" + default_prompt: "Use $wpf-visual-tree-cli to inspect a running WPF app safely from the command line." diff --git a/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md new file mode 100644 index 0000000..618c341 --- /dev/null +++ b/.agents/skills/wpf-visual-tree-cli/references/cli-reference.md @@ -0,0 +1,167 @@ +# WpfVisualTreeMcp CLI reference + +The command map in this reference matches the current WpfVisualTreeMcp repository. Treat the installed command's `help` output as authoritative; v0.12.0 does not contain every current option. Packaging and target-framework notes describe the current repository state; verify which later release first contains them. + +## Installation and mode selection + +```powershell +dotnet tool install --global WpfVisualTreeMcp +wpfinspect help +``` + +For an existing global-tool installation, inspect version drift without changing it: + +```powershell +dotnet tool list --global | Select-String '^wpfvisualtreemcp\s' +dotnet tool search WpfVisualTreeMcp --detail +``` + +When a newer stable release specifically addresses the observed limitation and the user approves the change: + +```powershell +dotnet tool update --global WpfVisualTreeMcp +wpfinspect help +``` + +This update command does not replace a manually extracted `WpfVisualTreeMcp.Server.exe`. Update that installation from GitHub Releases instead. + +The published v0.12.0 package and ZIP omit required Auto-injection files. Reinstalling v0.12.0 does not fix that defect. Prefer a later release whose notes include the native-bootstrapper and .NET Framework dependency-closure repair, or use a verified current source build or self-hosted mode. + +For an Auto-injection source build, build both native bootstrapper platforms with Visual Studio MSBuild before publishing the server; `dotnet build` alone skips the `.vcxproj`: + +```powershell +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 +dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --configuration Release --output ./publish +``` + +The release ZIP exposes the same CLI as `WpfVisualTreeMcp.Server.exe`. Starting the executable with no arguments runs the MCP stdio server; a recognized subcommand runs one CLI operation. + +Every command except `list` accepts `--pid ` or `--process `. Prefer `--pid`. Global options are `--compact`, `--verbose`, and `--help`/`-h`. + +## Command map + +### Discover and attach + +```text +list +attach --pid|--process [--auto-inject] +``` + +`attach` without `--auto-inject` connects only when the Inspector is already hosted or was injected earlier. `--auto-inject` loads it once into the current target process. + +### Find and inspect + +```text +tree --pid [--root H] [--depth N] +props --pid --handle H +find --pid [--type T] [--name N] [--text S] [--visible-only] [--root H] [--max N] [--filter JSON] +find-deep --pid (--type T | --name N | --text S) [--visible-only] [--root H] [--filter JSON] +bindings --pid --handle H +binding-errors --pid +data-context --pid --handle H +resources --pid [--scope application|element] [--handle H] +styles --pid --handle H +layout --pid --handle H +evaluate-binding --pid --handle H --property P +explain-triggers --pid --handle H [--property P] +``` + +`find` limits results to 50 by default. Filters combine with AND. `find-deep` requires at least one of type, name, or text so the search is bounded semantically. + +### Observe and compare + +```text +watch-property --pid --handle H --property P +wait-for --pid (--type T | --name N | --text S) [--condition visible|exists|enabled|hidden] [--timeout MS] [--poll MS] +snapshot --pid [--handle H] [--label L] [--depth N] +diff --pid --before L1 --after L2 +``` + +`watch-property` registers the watch, but the one-shot CLI cannot stream change events; re-read properties. `wait-for` defaults to a 10-second timeout and 250 ms poll interval, with a 25-second maximum timeout. + +### Capture and export + +```text +highlight --pid --handle H [--duration MS] +export --pid [--handle H] [--format json|xaml] [--out FILE] +screenshot --pid [--handle H] [--out FILE] [--max-width N] [--max-height N] [--mode render|screen] [--full-content] +``` + +`render` is the screenshot default and works if the window is covered, but it omits popup windows. `screen` captures visible popups, dropdowns, context menus, and tooltips but requires an unobstructed visible window. + +Add `--full-content` in render mode to capture all content in a `ScrollViewer`. Pass the most precise element handle because a control template or subtree can contain more than one ScrollViewer. Ordinary content is rendered directly; virtualized content is paged and stitched, with the original scroll offsets restored. Increase `--max-height` when the default limit would make a long image unreadably small. Full-content capture cannot be combined with `--mode screen`, and logically scrolling virtualized controls with horizontal overflow are unsupported. Output is area-downscaled to at most 8,388,608 pixels; capture fails safely if retained frames plus output exceed 67,108,864 pixels, the encoded PNG exceeds 33,554,432 bytes, or chunked work reaches the 25-second cooperative deadline. + +### Change application state + +```text +clear-binding-errors --pid +click --pid --handle H [--physical] [--click-type single|double|right] +select-item --pid --handle H (--item-text S | --index N) +set-text --pid --handle H --text VALUE [--physical] +send-keys --pid --keys COMBO [--handle H] +set-property --pid --handle H --property P --value V +revert-property --pid (--all | [--handle H] [--property P]) +``` + +`set-property` replaces a binding with a local value when applied to a bound dependency property. `revert-property` restores the prior binding, local value, or default. + +## Read-only diagnostic example + +```powershell +$wpfProcessList = wpfinspect list --compact | ConvertFrom-Json +$targetProcessId = ($wpfProcessList.processes | Where-Object processName -eq 'MyApp' | Select-Object -First 1).processId + +$attachResult = wpfinspect attach --pid $targetProcessId --compact | ConvertFrom-Json +if ($attachResult.inspectorStatus -like 'Not loaded*') { + wpfinspect attach --pid $targetProcessId --auto-inject +} + +$buttons = wpfinspect find --pid $targetProcessId --type Button --text Save --visible-only --compact | ConvertFrom-Json +wpfinspect binding-errors --pid $targetProcessId +``` + +Check for multiple matching processes before using `Select-Object -First 1`; do not silently choose one in an ambiguous live environment. + +## Reversible experiment example + +```powershell +wpfinspect snapshot --pid $targetProcessId --label before +wpfinspect set-property --pid $targetProcessId --handle elem_00000052 --property Width --value 300 +wpfinspect snapshot --pid $targetProcessId --label after +wpfinspect diff --pid $targetProcessId --before before --after after +wpfinspect revert-property --pid $targetProcessId --handle elem_00000052 --property Width +``` + +## Self-hosted startup shape + +Use only with an Inspector build compatible with the target application's framework. Current source provides `net472`, `net48`, and `net8.0-windows` Inspector targets: + +```csharp +using System.Diagnostics; +using System.Windows; +using WpfVisualTreeMcp.Inspector; + +public partial class App : Application +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + InspectorService.Initialize(Process.GetCurrentProcess().Id); + } + + protected override void OnExit(ExitEventArgs e) + { + InspectorService.Instance?.Dispose(); + base.OnExit(e); + } +} +``` + +After self-hosting, run `attach` without `--auto-inject`. + +## Sources + +- Repository: +- NuGet tool: +- CLI implementation for v0.12.0: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83a774c..840fd40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,34 +17,132 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v3 + + - name: Build native bootstrapper + run: | + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + - name: Restore dependencies run: dotnet restore WpfVisualTreeMcp.sln + - name: Publish MCP Server from clean checkout + run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-restore --configuration Release --output ./publish + - name: Build run: dotnet build WpfVisualTreeMcp.sln --no-restore --configuration Release - name: Test run: dotnet test WpfVisualTreeMcp.sln --no-build --configuration Release --verbosity normal --logger "trx;LogFileName=test-results.trx" + - name: Install x86 .NET runtime + shell: pwsh + run: | + $dotnetRoot = Join-Path $env:RUNNER_TEMP 'dotnet-x86' + $installScript = Join-Path $env:RUNNER_TEMP 'dotnet-install.ps1' + Invoke-WebRequest https://dot.net/v1/dotnet-install.ps1 -OutFile $installScript + & $installScript -Channel 8.0 -Runtime dotnet -Architecture x86 -InstallDir $dotnetRoot -NoPath + & $installScript -Channel 8.0 -Runtime windowsdesktop -Architecture x86 -InstallDir $dotnetRoot -NoPath + if (-not (Test-Path -LiteralPath (Join-Path $dotnetRoot 'dotnet.exe'))) { throw 'x86 .NET runtime installation failed.' } + "DOTNET_ROOT_X86=$dotnetRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Test WPF inspection matrix + shell: pwsh + run: ./tests/run-integration-tests.ps1 -SkipNativeBuild + - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: test-results path: '**/TestResults/*.trx' - - name: Publish MCP Server - run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./publish + - name: Verify auto-injection payload + shell: pwsh + run: | + $netFxInspectorDlls = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll + $requiredPayloadFiles = @( + './publish/native/x64/WpfInspectorBootstrapper.dll' + './publish/native/x86/WpfInspectorBootstrapper.dll' + './publish/native/x86/WpfInjectorHelper.exe' + './publish/native/x86/WpfInjectorHelper.dll' + './publish/native/x86/WpfInjectorHelper.deps.json' + './publish/native/x86/WpfInjectorHelper.runtimeconfig.json' + './publish/native/x86/WpfVisualTreeMcp.Injector.dll' + './publish/native/x64/coreclr/WpfVisualTreeMcp.Inspector.dll' + './publish/native/x64/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/native/x64/coreclr/WpfVisualTreeMcp.Shared.dll' + './publish/native/x86/coreclr/WpfVisualTreeMcp.Inspector.dll' + './publish/native/x86/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/native/x86/coreclr/WpfVisualTreeMcp.Shared.dll' + foreach ($architecture in @('x64', 'x86')) { + foreach ($dll in $netFxInspectorDlls) { + "./publish/native/$architecture/$($dll.Name)" + } + } + ) + $missingPayloadFiles = @($requiredPayloadFiles | Where-Object { -not (Test-Path -LiteralPath $_) }) + if ($missingPayloadFiles.Count -gt 0) { + throw "Missing auto-injection payload files: $($missingPayloadFiles -join ', ')" + } + + - name: Pack NuGet package + run: dotnet pack src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./artifacts + + - name: Verify NuGet auto-injection payload + shell: pwsh + run: | + $package = Get-ChildItem ./artifacts/*.nupkg | Select-Object -First 1 + if ($null -eq $package) { throw 'NuGet package was not created.' } + + $netFxInspectorDllNames = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll | + Select-Object -ExpandProperty Name + $requiredEntrySuffixes = @( + foreach ($architecture in @('x64', 'x86')) { + "/native/$architecture/WpfInspectorBootstrapper.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Shared.dll" + foreach ($dllName in $netFxInspectorDllNames) { + "/native/$architecture/$dllName" + } + } + '/native/x86/WpfInjectorHelper.exe' + '/native/x86/WpfInjectorHelper.dll' + '/native/x86/WpfInjectorHelper.deps.json' + '/native/x86/WpfInjectorHelper.runtimeconfig.json' + '/native/x86/WpfVisualTreeMcp.Injector.dll' + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName) + try { + $entryNames = @($archive.Entries.FullName) + $missingEntrySuffixes = @($requiredEntrySuffixes | Where-Object { + $requiredSuffix = $_ + -not ($entryNames | Where-Object { + $_.EndsWith($requiredSuffix, [StringComparison]::OrdinalIgnoreCase) + }) + }) + if ($missingEntrySuffixes.Count -gt 0) { + throw "NuGet package is missing auto-injection payload entries: $($missingEntrySuffixes -join ', ')" + } + } + finally { + $archive.Dispose() + } - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wpf-visual-tree-mcp path: ./publish @@ -54,20 +152,22 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - # Server transitively references the WPF (net8.0-windows) Inspector, so a + # Server has a build-only reference to the WPF Inspector payload, so a # restore/format on Linux needs Windows targeting enabled. - name: Restore dependencies run: dotnet restore src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -p:EnableWindowsTargeting=true - name: Check formatting run: dotnet format src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --verify-no-changes --verbosity diagnostic + env: + EnableWindowsTargeting: true continue-on-error: true # Build verification for .NET Framework projects (Windows only) @@ -76,21 +176,22 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.3 + uses: microsoft/setup-msbuild@v3 - name: Restore NuGet packages run: dotnet restore WpfVisualTreeMcp.sln - - name: Build .NET Framework projects + - name: Build .NET Framework targets run: | - dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release - dotnet build src/WpfVisualTreeMcp.Injector/WpfVisualTreeMcp.Injector.csproj --configuration Release - dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release + dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net472 + dotnet build src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net48 + dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release --framework net472 + dotnet build samples/SampleWpfApp/SampleWpfApp.csproj --configuration Release --framework net48 diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml index 94de03c..19c02c8 100644 --- a/.github/workflows/publish-mcp-registry.yml +++ b/.github/workflows/publish-mcp-registry.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Download mcp-publisher run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a68594..8570fb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,27 +22,61 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.3 + uses: microsoft/setup-msbuild@v3 + + - name: Build native bootstrapper + run: | + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 - name: Restore dependencies run: dotnet restore WpfVisualTreeMcp.sln + - name: Publish MCP Server from clean checkout + run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-restore --configuration Release --output ./publish/server + - name: Build Release run: dotnet build WpfVisualTreeMcp.sln --no-restore --configuration Release - name: Test run: dotnet test WpfVisualTreeMcp.sln --no-build --configuration Release --verbosity normal - - name: Publish MCP Server - run: dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --no-build --configuration Release --output ./publish/server + - name: Verify auto-injection payload + shell: pwsh + run: | + $netFxInspectorDlls = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll + $requiredPayloadFiles = @( + './publish/server/native/x64/WpfInspectorBootstrapper.dll' + './publish/server/native/x86/WpfInspectorBootstrapper.dll' + './publish/server/native/x86/WpfInjectorHelper.exe' + './publish/server/native/x86/WpfInjectorHelper.dll' + './publish/server/native/x86/WpfInjectorHelper.deps.json' + './publish/server/native/x86/WpfInjectorHelper.runtimeconfig.json' + './publish/server/native/x86/WpfVisualTreeMcp.Injector.dll' + './publish/server/native/x64/coreclr/WpfVisualTreeMcp.Inspector.dll' + './publish/server/native/x64/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/server/native/x64/coreclr/WpfVisualTreeMcp.Shared.dll' + './publish/server/native/x86/coreclr/WpfVisualTreeMcp.Inspector.dll' + './publish/server/native/x86/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json' + './publish/server/native/x86/coreclr/WpfVisualTreeMcp.Shared.dll' + foreach ($architecture in @('x64', 'x86')) { + foreach ($dll in $netFxInspectorDlls) { + "./publish/server/native/$architecture/$($dll.Name)" + } + } + ) + $missingPayloadFiles = @($requiredPayloadFiles | Where-Object { -not (Test-Path -LiteralPath $_) }) + if ($missingPayloadFiles.Count -gt 0) { + throw "Missing auto-injection payload files: $($missingPayloadFiles -join ', ')" + } - name: Publish Inspector DLL run: dotnet publish src/WpfVisualTreeMcp.Inspector/WpfVisualTreeMcp.Inspector.csproj --configuration Release --framework net8.0-windows --output ./publish/inspector @@ -54,6 +88,49 @@ jobs: - name: Pack NuGet package run: dotnet pack src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj --configuration Release --output ./artifacts + - name: Verify NuGet auto-injection payload + shell: pwsh + run: | + $package = Get-ChildItem ./artifacts/*.nupkg | Select-Object -First 1 + if ($null -eq $package) { throw 'NuGet package was not created.' } + + $netFxInspectorDllNames = Get-ChildItem ./src/WpfVisualTreeMcp.Inspector/bin/Release/net48/*.dll | + Select-Object -ExpandProperty Name + $requiredEntrySuffixes = @( + foreach ($architecture in @('x64', 'x86')) { + "/native/$architecture/WpfInspectorBootstrapper.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.dll" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Inspector.runtimeconfig.json" + "/native/$architecture/coreclr/WpfVisualTreeMcp.Shared.dll" + foreach ($dllName in $netFxInspectorDllNames) { + "/native/$architecture/$dllName" + } + } + '/native/x86/WpfInjectorHelper.exe' + '/native/x86/WpfInjectorHelper.dll' + '/native/x86/WpfInjectorHelper.deps.json' + '/native/x86/WpfInjectorHelper.runtimeconfig.json' + '/native/x86/WpfVisualTreeMcp.Injector.dll' + ) + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName) + try { + $entryNames = @($archive.Entries.FullName) + $missingEntrySuffixes = @($requiredEntrySuffixes | Where-Object { + $requiredSuffix = $_ + -not ($entryNames | Where-Object { + $_.EndsWith($requiredSuffix, [StringComparison]::OrdinalIgnoreCase) + }) + }) + if ($missingEntrySuffixes.Count -gt 0) { + throw "NuGet package is missing auto-injection payload entries: $($missingEntrySuffixes -join ', ')" + } + } + finally { + $archive.Dispose() + } + # Trusted publishing (OIDC): requires a policy at nuget.org/account/trustedpublishing # for repo faze79/WPFVisualTreeMcp, workflow release.yml, package WpfVisualTreeMcp, # plus a repo secret NUGET_USER = the nuget.org profile name. Skipped with a notice @@ -94,7 +171,7 @@ jobs: } - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: body_path: release-notes.md files: | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3cdc603 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS.md + +Read and follow [CLAUDE.md](CLAUDE.md) as the repository's authoritative agent guidance. diff --git a/CHANGELOG.md b/CHANGELOG.md index f28ff8c..e101294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Capture complete `ScrollViewer` content with `wpf_capture_screenshot(full_content=true)` + or `screenshot --full-content`, including vertically virtualized items, while restoring + the original scroll position after capture. +- Target .NET Framework 4.7.2, .NET Framework 4.8, and .NET 8 for Windows from the + Inspector, Shared, sample, and 12-case Self-hosted/Auto-injection integration matrix. + +### Fixed + +- Build and package the x64 and x86 native bootstrappers so Auto-injection works from the + NuGet tool and release archive. +- Package the complete .NET Framework Inspector dependency closure and resolve its + co-located private assemblies only while the Inspector payload chain is active, including + runtime requests without requester metadata, without relying on the target application's + binding redirects or changing unrelated application binds. +- Use the standard CoreCLR runtimeconfig filename so it remains accessible from the NuGet + tool's deeply nested installation directory. +- Collect the injection payload after project references build so clean publish and pack + operations cannot silently omit managed assemblies or the x86 helper, and validate every + declared payload entry. +- Treat the Inspector named pipe, rather than a loaded module, as the attachment readiness + signal. +- Bound direct and stitched full-content screenshot raw, PNG, and base64 memory; split + rendering, composition, and encoding into cancellation-aware chunks; and derive + logical-page displacement from realized item-container geometry so identical or + variable-height adjacent frames stitch correctly. + ## [0.12.0] - 2026-07-24 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 783f9d4..fba1751 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,17 +9,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build Commands ```bash -# Build solution +# Build managed solution dotnet build WpfVisualTreeMcp.sln -# Build for Release +# Build managed solution for Release dotnet build -c Release WpfVisualTreeMcp.sln +# Build native auto-injection bootstrapper (Visual Studio MSBuild) +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + # Run tests dotnet test WpfVisualTreeMcp.sln +# Run the full self-hosted/auto-injection matrix (Windows PowerShell) +./tests/run-integration-tests.ps1 + # Run sample WPF app for testing -dotnet run --project samples/SampleWpfApp +dotnet run --project samples/SampleWpfApp --framework net8.0-windows # Publish MCP Server executable (same exe also runs as the CLI) dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -c Release -o ./publish @@ -39,8 +46,8 @@ MCP Server (.NET 8.0) ├─ ProcessManager (discovers WPF processes) └─ NamedPipeBridge (IPC) ↓ [Named Pipes: wpf_inspector_{pid}] -Target WPF Application (.NET Framework) - └─ Inspector DLL (.NET 4.8) +Target WPF Application (.NET Framework 4.7.2/4.8 or .NET 8) + └─ Inspector DLL (compatible framework payload) ├─ TreeWalker (visual tree navigation + adorner/popup traversal) ├─ ScreenshotCapture (RenderTargetBitmap element capture) ├─ PropertyReader (dependency properties) @@ -49,7 +56,7 @@ Target WPF Application (.NET Framework) └─ IpcServer (named pipe communication) ``` -**Key Design:** Multi-process architecture for safety. The server runs separately and communicates via named pipes. All operations are read-only **except `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, and `wpf_revert_property`**, which drive controls or edit property values and change application state. `wpf_set_property` is reversible via `wpf_revert_property` (per-session undo stack that restores the prior binding, local value, or default). +**Key Design:** Multi-process architecture for safety. The server runs separately and communicates via named pipes. The six interaction/property commands — `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, and `wpf_revert_property` — drive controls or edit property values and change application state. `wpf_set_property` is reversible via `wpf_revert_property` (per-session undo stack that restores the prior binding, local value, or default). `wpf_highlight_element` temporarily changes the UI, while `wpf_clear_binding_errors` clears Inspector-held diagnostics. ## Key Source Locations @@ -65,7 +72,7 @@ Target WPF Application (.NET Framework) | Injector Helper | `src/WpfVisualTreeMcp.InjectorHelper/Program.cs` | 32-bit .NET 8 helper exe spawned by `ProcessInjector` for cross-arch injection (v0.6.0) | | IPC Bridge | `src/WpfVisualTreeMcp.Server/Services/NamedPipeBridge.cs` | Named pipe communication to Inspector | | Process Manager | `src/WpfVisualTreeMcp.Server/Services/ProcessManager.cs` | WPF process discovery and attachment | -| Inspector Entry | `src/WpfVisualTreeMcp.Inspector/InspectorService.cs` | Injected DLL main entry point | +| Inspector Entry | `src/WpfVisualTreeMcp.Inspector/InspectorService.cs` | Injected or self-hosted DLL main entry point | | Screenshot Capture | `src/WpfVisualTreeMcp.Inspector/ScreenshotCapture.cs` | RenderTargetBitmap element/window capture | | IPC Server | `src/WpfVisualTreeMcp.Inspector/IpcServer.cs` | Named pipe server in target process | | IPC Messages | `src/WpfVisualTreeMcp.Shared/Ipc/IpcMessages.cs` | Request/response contracts | @@ -129,6 +136,17 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d dropdowns, context menus and tooltips — requires the window visible and unobstructed) - DPI-aware via `PresentationSource.FromVisual` - Downscales if exceeding `max_width`/`max_height` (default 1920x1080) +- Captures the element's current arranged bounds by default; `full_content=true` + (render mode only) locates the target's largest ScrollViewer, renders ordinary + content directly, or pages and stitches virtualized content before restoring offsets +- Full-content capture supports physical scrolling in both dimensions and logical + virtualized scrolling vertically; logical virtualized horizontal overflow is rejected +- Full-content output is area-downscaled to at most 8,388,608 pixels; capture rejects + retained-frame-plus-output allocations above 67,108,864 pixels and encoded PNGs above + 33,554,432 bytes, avoiding unbounded bitmap, PNG, base64, and JSON memory growth +- Full-content rendering, composition, and encoding use cancellation-aware bounded chunks + under a 25-second cooperative deadline that starts before Dispatcher scheduling, + leaving headroom below the 30-second named-pipe request timeout - Returns MCP `ImageContentBlock` (base64 PNG) — Claude sees the image directly ### Logging @@ -140,10 +158,12 @@ The Inspector strips UTF-8 BOM (0xEF 0xBB 0xBF) before JSON parsing to prevent d ## WPF App Inspection Modes ### Auto-Injection Mode -Use `wpf_attach(process_id=, auto_inject=true)` to inject the Inspector into any running .NET Framework WPF app. Requires: +Use `wpf_attach(process_id=, auto_inject=true)` to inject the Inspector into a running .NET Framework or .NET 8 WPF app. Requires: - Native bootstrapper DLL in `publish/native/x64/` (or x86) -- Target process must be .NET Framework (CLR hosting) -- Architecture detection is automatic (x64 vs x86) +- A supported CLR and matching Inspector payload (`net48` for .NET Framework or `net8.0-windows` for CoreCLR) +- Architecture detection and payload selection use the target process (x64 vs x86) +- .NET Framework private dependency resolution is limited to the Inspector payload chain + in the Inspector directory; do not broaden it to unrelated target-application binds ### Self-Hosted Mode For your own WPF application, add a reference to the Inspector and initialize on startup: @@ -157,6 +177,10 @@ protected override void OnStartup(StartupEventArgs e) } ``` +The Inspector multi-targets `net472`, `net48`, and `net8.0-windows`, so a project +reference selects the build matching the WPF application's target framework. +Self-hosting avoids runtime injection but requires modifying the application. + ## MCP Server Configuration The server uses the official Microsoft/Anthropic MCP SDK. Configure in `.mcp.json`: @@ -177,7 +201,7 @@ The server uses the official Microsoft/Anthropic MCP SDK. Configure in `.mcp.jso The server executable doubles as a command-line tool. `WpfVisualTreeMcp.Server.exe` with **no arguments** runs the MCP stdio server; with **any recognised subcommand** it runs a single one-shot CLI command instead (`Program.cs` checks `args[0]` via -`CliRunner.IsCliCommand`). This gives the same 21 capabilities without an MCP +`CliRunner.IsCliCommand`). This gives the same 28 capabilities without an MCP connection — useful when the MCP server is not connected, for scripting, or for verifying the pipeline manually. @@ -194,7 +218,9 @@ shell call. No MCP handshake required; `--help` is self-documenting. - **Targeting:** every command except `list` takes `--pid ` or `--process `. - **screenshot** writes a PNG file and prints its path (Claude reads it with Read); **export** writes to `--out` if given, otherwise prints content inline. -- **click / select-item / set-text / send-keys** are the four state-changing commands. +- **click / select-item / set-text / send-keys / set-property / revert-property** + are the six commands that change application state. `highlight` changes the UI + temporarily, and `clear-binding-errors` clears Inspector-held diagnostics. - `click` — UI Automation invoke by default; `--physical` for OS mouse click (auto-scrolls into view); `--click-type double|right` for double/right clicks (always physical; right opens context menus — capture with `screenshot --mode screen`). @@ -206,7 +232,7 @@ shell call. No MCP handshake required; `--help` is self-documenting. The response reports the value read back after the write. - `send-keys` — OS-level keyboard input; modifiers `Ctrl/Shift/Alt/Win` plus letters, digits, F1-F12, and named keys. - - All four live in `ControlInteractor`. + - The first four live in `ControlInteractor`; property edits use `PropertyWriter`. ### Typical CLI workflow ```bash @@ -226,14 +252,24 @@ Run `WpfVisualTreeMcp.Server.exe help` for the full command list, or (registry manifest — must match the NuGet package version exactly) 3. Update `CHANGELOG.md` and `RELEASE_NOTES.md` 4. Commit, tag `vX.Y.Z`, push tag → `release.yml` builds the zip, creates the GitHub - release, packs the NuGet package and pushes it to nuget.org (needs the - `NUGET_API_KEY` repo secret; skipped with a notice if absent) + release, packs the NuGet package and pushes it to nuget.org using trusted + publishing (requires the nuget.org policy and `NUGET_USER` repo secret; + skipped with a notice if the secret is absent) 5. Once the package is live on nuget.org, run the manual **"Publish to MCP Registry"** workflow (GitHub OIDC, no secrets) to update registry.modelcontextprotocol.io +The end-user CLI skill is stored under `.agents/skills/wpf-visual-tree-cli/`. +Keep its command map, installation guidance, inspection modes, and limitations in +sync with the CLI help, README, and release notes. + ## Test Framework - **xUnit** - Test runner - **Moq** - Mocking - **FluentAssertions** - Assertions -- Tests located in `tests/WpfVisualTreeMcp.Tests/` +- Server and Injector tests: `tests/WpfVisualTreeMcp.Tests/` (`net8.0`) +- Shared IPC/model tests: `tests/WpfVisualTreeMcp.Shared.Tests/` + (`net472`, `net48`, and `net8.0`) +- Live framework/architecture/mode matrix: `tests/WpfVisualTreeMcp.IntegrationTests/` + (run through `tests/run-integration-tests.ps1`; Auto-injection cases publish the sample + without an Inspector reference so they exercise only the packaged payload) diff --git a/README.md b/README.md index 5f02ff6..89ad58b 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ Debugging WPF UI issues traditionally requires manual inspection with specialize ### Search & Monitoring - **Element Search** - Find elements by type, name, or property values - **Deep Search** - Search entire tree including AdornerLayer and Popup elements -- **Property Watching** - Monitor property changes in real-time +- **Property Watching** - Register property watches and read their initial values (streaming client notifications are planned) ### Interaction & Export -- **Screenshot Capture** - Capture window/element screenshots visible to AI agents +- **Screenshot Capture** - Capture window/element screenshots, including complete ScrollViewer content and virtualized items - **Element Highlighting** - Visually highlight elements in the running app - **Control Click** *(v0.4.0)* - Click elements via UI Automation (`Invoke`/`Toggle`/`Select`/`ExpandCollapse`) or a real OS mouse click - **Set Text** *(new in v0.5.0)* - Fill a TextBox/ComboBox/PasswordBox via UI Automation `IValueProvider.SetValue`, with a `TextBox.Text` / `PasswordBox.Password` / reflected-`Text` fallback, or `physical=true` to type via OS keyboard input (full Unicode BMP) @@ -95,7 +95,7 @@ command list. Output is JSON on stdout; diagnostics go to stderr. | Find controls by visible text / properties | ✅ | manual | partial (UIA) | pixel guessing | | Click / type / select / shortcuts | ✅ | ❌ | ✅ | ✅ (blind) | | Wait for UI conditions (no sleep loops) | ✅ | ❌ | ✅ | ❌ | -| Element screenshots + popup-aware screen capture | ✅ | ❌ | partial | full screen only | +| Element/full-scroll screenshots + popup-aware screen capture | ✅ | ❌ | partial | full screen only | | Works without target source changes | ✅ (auto-injection) | ✅ | ✅ | ✅ | **vs. other WPF MCP servers.** Most WPF MCP servers are built on **UI Automation** (FlaUI): they read the *accessibility* tree, and can only reach WPF internals — bindings, DataContext, ViewModel state — if you **install their in-process probe into the app you want to inspect**. This server takes the Snoop route instead: it **injects at runtime**, so it reads the *real* visual tree and diagnoses binding errors and DataContext **with zero changes to the target app** — nothing to add to your build, nothing to ship into production. @@ -107,7 +107,7 @@ command list. Output is JSON on stdout; diagnostics go to stderr. | Interaction surface | click (UIA + physical, double/right), set-text w/ read-back, send-keys, **select-item (virtualized)** | click / type / select | usually invoke-only | | Wait for element conditions | ✅ `wpf_wait_for` | ✅ | ❌ | | Popup / dropdown / context-menu screenshots | ✅ screen mode | partial | screenshot only | -| Cross-architecture injection (x64 ⇄ x86) | ✅ | n/a | ❌ | +| Cross-architecture injection (x64 server → x86 target) | ✅ | n/a | ❌ | | Dual-mode: MCP server **and** one-shot CLI | ✅ | ❌ | ❌ | | Distribution | NuGet (`dnx`/tool) + official MCP registry | varies | varies | @@ -120,6 +120,8 @@ In short: UIA-based tools see what accessibility exposes, and computer-use agent - Windows 10/11 - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later - A WPF application to inspect +- Visual Studio 2022 C++ build tools when building the auto-injection payload from source +- x86 .NET 8 Windows Desktop runtime when running the cross-bitness integration tests ### Installation @@ -149,9 +151,14 @@ Download the latest release from [GitHub Releases](https://github.com/faze79/Wpf ```bash git clone https://github.com/faze79/WpfVisualTreeMcp.git cd WpfVisualTreeMcp -dotnet build -c Release +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 +msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 +dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -c Release -o ./publish ``` +The native steps require Visual Studio Build Tools with Desktop development +with C++. A managed `dotnet build` is sufficient when Auto-injection is not needed. + ### Configuration The server uses the **official Microsoft/Anthropic MCP SDK for .NET**, providing guaranteed compatibility with Claude Code and other MCP clients. @@ -208,7 +215,8 @@ Add to `~/.claude/settings.json`: **Important Notes:** - Use absolute paths to the built `.exe` file - Use forward slashes (`/`) in paths on Windows -- Build in Release mode for production: `dotnet build -c Release` +- Use the Release publish output for production; source-built Auto-injection also + requires both native bootstrapper builds shown above - Restart Claude Code after configuration changes #### Cursor @@ -226,7 +234,36 @@ Add to your Cursor settings (`.cursor/mcp.json`): } ``` -### Self-Hosted Mode (Recommended) +### Auto-Injection Mode + +Auto-injection loads the Inspector into an already-running WPF process without +source changes. Set `auto_inject=true` when calling `wpf_attach`, or run: + +```powershell +wpfinspect attach --pid --auto-inject +``` + +Auto-injection has these constraints: + +- It requires permission to open the target process, write memory, and create a + remote thread. Elevated, protected, sandboxed, or security-hardened processes + may reject it, and endpoint security may block it as DLL injection. +- The matching x64 or x86 native bootstrapper and the complete Inspector + dependency set must be present. A 64-bit server additionally needs the + bundled x86 helper and the x86 .NET 8 runtime to inject into a 32-bit target. + Native ARM64 targets are not supported. +- Injection occurs after process startup, so it cannot recover earlier binding + errors or initialization activity. A restarted application must be injected + again under its new process ID. +- The target must have an initialized WPF `Application` and a responsive UI + dispatcher. The injected Inspector remains loaded until the target exits. +- Loading native and managed code into the target can conflict with its runtime, + assembly versions, or process-hardening policy. + +See the [injector documentation](src/WpfVisualTreeMcp.Injector/README.md) for the +implementation, runtime requirements, diagnostics, and detailed limitations. + +### Self-Hosted Mode For your WPF application to be inspectable, add a reference to the Inspector DLL and initialize it on startup: @@ -258,6 +295,8 @@ public partial class App : Application ``` This enables the MCP server to connect to your application via named pipes for real-time inspection. +The Inspector multi-targets .NET Framework 4.7.2, .NET Framework 4.8, and +.NET 8 for Windows, so project references select a compatible build. ## Usage Examples @@ -334,7 +373,7 @@ For detailed architecture documentation, see [docs/ARCHITECTURE.md](docs/ARCHITE | `wpf_get_element_properties` | Get all dependency properties of an element | | `wpf_find_elements` | Query elements by type, x:Name, **visible text**, property values and visibility; results include text, automation id, enabled/visible state and screen bounds | | `wpf_find_elements_deep` | Same query filters without result limit, across all windows including adorners/popups | -| `wpf_capture_screenshot` | Capture a screenshot of the window or element (returns image); `mode='screen'` captures real on-screen pixels including open popups, dropdowns and context menus | +| `wpf_capture_screenshot` | Capture a screenshot of the window or element (returns image); `full_content=true` captures complete ScrollViewer content; `mode='screen'` captures real on-screen pixels including open popups, dropdowns and context menus | | `wpf_get_bindings` | Get data bindings for an element (includes MultiBinding, converter, StringFormat) | | `wpf_get_binding_errors` | List all captured binding errors | | `wpf_clear_binding_errors` | Clear the captured binding errors list | @@ -357,7 +396,21 @@ For detailed architecture documentation, see [docs/ARCHITECTURE.md](docs/ARCHITE | `wpf_get_layout_info` | Get layout information | | `wpf_export_tree` | Export visual tree to XAML or JSON | -For complete tool documentation, see [docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERENCE.md). +Screenshot capture covers the element's current arranged bounds by default. Set +`full_content=true` with render mode to capture all content in a `ScrollViewer`. +Non-virtualized content is rendered directly; virtualized content is paged and +stitched, then the original scroll position is restored. Logically scrolling, +virtualized controls with horizontal overflow are not currently supported by +full-content capture. Increase `max_height` when a long image would otherwise be +downscaled too far. Full-content output is area-downscaled to at most 8,388,608 +pixels. Capture fails safely if retained frames plus the final image exceed the +67,108,864-pixel raw-bitmap budget, the encoded PNG exceeds 33,554,432 bytes, or +chunked render, composition, and encoding work reaches its 25-second cooperative +execution deadline. + +For detailed examples of the original inspection tools, see +[docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERENCE.md); run `wpfinspect help` for +complete CLI documentation. ## Roadmap @@ -374,7 +427,7 @@ For complete tool documentation, see [docs/TOOLS_REFERENCE.md](docs/TOOLS_REFERE - [x] Binding analysis and error detection - [x] Resource dictionary enumeration - [x] Style and template inspection -- [x] Property change monitoring (with notifications) +- [x] Property watch registration and initial-value capture ### Phase 3: Interaction & Diagnostics ✅ - [x] Element highlighting overlay @@ -450,13 +503,16 @@ Contributions are welcome! Please feel free to submit a Pull Request. 3. Build and run tests: ```bash - dotnet build - dotnet test + dotnet build WpfVisualTreeMcp.sln -c Release + dotnet test WpfVisualTreeMcp.sln -c Release + + # Full 12-case self-hosted/auto-injection matrix (Windows PowerShell) + ./tests/run-integration-tests.ps1 ``` 4. Run the sample WPF app for testing: ```bash - dotnet run --project samples/SampleWpfApp + dotnet run --project samples/SampleWpfApp --framework net8.0-windows ``` ### Project Structure @@ -466,18 +522,20 @@ WpfVisualTreeMcp/ ├── src/ │ ├── WpfVisualTreeMcp.Server/ # MCP Server (.NET 8) - Uses official MCP SDK │ │ ├── Program.cs # Server initialization with MCP SDK -│ │ ├── WpfTools.cs # 20 WPF tools (17 inspection + click/set-text/send-keys) +│ │ ├── WpfTools.cs # 28 WPF tools (22 inspection + 6 state-changing) │ │ ├── Cli/CliRunner.cs # One-shot CLI front-end (v0.4.0) │ │ └── Services/ # Process & IPC management -│ ├── WpfVisualTreeMcp.Inspector/ # Injected DLL (.NET Framework 4.8) -│ ├── WpfVisualTreeMcp.Injector/ # Managed injection logic (CreateRemoteThread; net48 + net8.0) +│ ├── WpfVisualTreeMcp.Inspector/ # Injected DLL (.NET Framework 4.7.2/4.8 and .NET 8) +│ ├── WpfVisualTreeMcp.Injector/ # Managed injection logic (CreateRemoteThread; .NET 8) │ ├── WpfVisualTreeMcp.InjectorHelper/# x86 .NET 8 helper exe for cross-arch injection (v0.6.0) │ ├── WpfVisualTreeMcp.Bootstrapper/ # Native C++ DLL for CLR hosting │ └── WpfVisualTreeMcp.Shared/ # Shared models & IPC contracts ├── samples/ │ └── SampleWpfApp/ # Test application ├── tests/ -│ └── WpfVisualTreeMcp.Tests/ # Unit tests (48 tests) +│ ├── WpfVisualTreeMcp.Tests/ # Server, CLI and injector unit tests +│ ├── WpfVisualTreeMcp.Shared.Tests/ # Shared contract tests across all target frameworks +│ └── WpfVisualTreeMcp.IntegrationTests/ # 3 frameworks × 2 architectures × 2 modes ├── publish/ # Published server + native DLLs │ └── native/{x64,x86}/ # Architecture-specific bootstrapper └── docs/ # Documentation @@ -487,7 +545,7 @@ WpfVisualTreeMcp/ - **MCP SDK**: Built with the [official C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk) from Microsoft/Anthropic - **Protocol**: JSON-RPC 2.0 over stdio transport -- **Target Framework**: .NET 8.0 (Server) / .NET Framework 4.8 + .NET 8.0-windows (Inspector, dual-target) +- **Target Framework**: .NET 8.0 (Server/Injector) / .NET Framework 4.7.2, 4.8 + .NET 8.0-windows (Inspector) - **IPC**: Named Pipes for server-to-application communication - **Tools**: 28 tools auto-discovered via `[McpServerTool]` attributes (22 read-only inspection incl. `wpf_wait_for`, `wpf_snapshot`, `wpf_diff`, `wpf_evaluate_binding`, `wpf_explain_triggers` + 6 state-changing: `wpf_click_element`, `wpf_select_item`, `wpf_set_text`, `wpf_send_keys`, `wpf_set_property`, `wpf_revert_property`) - **CLI**: same executable runs as one-shot CLI when given a subcommand (`Program.cs` routes via `CliRunner.IsCliCommand`) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 825edeb..55079e5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # Release Notes +> **v0.12.0 packaging note:** the published NuGet tool and official ZIP omit the +> native bootstrapper and managed Auto-injection dependency closure. The release +> description below reflects the intended feature set, but Auto-injection from +> those artifacts is incomplete. Use a later release containing the packaging +> repair, a verified current source build, or self-hosted mode. + ## v0.12.0 — Trigger & style diagnostics (2026-07-24) If you've debugged WPF, you've done this by hand: crack open the visual tree, hunt through a `Style` or `ControlTemplate`, and try to work out why a trigger isn't reacting — or which setter, in which style or trigger, actually produced a value. This release does it for you. diff --git a/WpfVisualTreeMcp.sln b/WpfVisualTreeMcp.sln index f7d05a8..d60563b 100644 --- a/WpfVisualTreeMcp.sln +++ b/WpfVisualTreeMcp.sln @@ -31,6 +31,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution mcp.json = mcp.json EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfVisualTreeMcp.Shared.Tests", "tests\WpfVisualTreeMcp.Shared.Tests\WpfVisualTreeMcp.Shared.Tests.csproj", "{17056590-7FA9-414F-9772-6C60C56C1A5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfVisualTreeMcp.IntegrationTests", "tests\WpfVisualTreeMcp.IntegrationTests\WpfVisualTreeMcp.IntegrationTests.csproj", "{A8E70F1C-2D31-4CF1-B589-1A290781D415}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,6 +69,14 @@ Global {F6A7B8C9-D0E1-2345-F012-456789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6A7B8C9-D0E1-2345-F012-456789012345}.Release|Any CPU.Build.0 = Release|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17056590-7FA9-414F-9772-6C60C56C1A5D}.Release|Any CPU.Build.0 = Release|Any CPU + {A8E70F1C-2D31-4CF1-B589-1A290781D415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8E70F1C-2D31-4CF1-B589-1A290781D415}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8E70F1C-2D31-4CF1-B589-1A290781D415}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8E70F1C-2D31-4CF1-B589-1A290781D415}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +89,8 @@ Global {B0A1C2D3-E4F5-6789-0123-456789ABCDEF} = {1A2B3C4D-5E6F-7890-1234-567890ABCDEF} {E5F6A7B8-C9D0-1234-EF01-345678901234} = {2B3C4D5E-6F78-9012-3456-7890ABCDEF01} {F6A7B8C9-D0E1-2345-F012-456789012345} = {3C4D5E6F-7890-1234-5678-90ABCDEF0123} + {17056590-7FA9-414F-9772-6C60C56C1A5D} = {3C4D5E6F-7890-1234-5678-90ABCDEF0123} + {A8E70F1C-2D31-4CF1-B589-1A290781D415} = {3C4D5E6F-7890-1234-5678-90ABCDEF0123} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {12345678-90AB-CDEF-1234-567890ABCDEF} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 088fdb8..7eef07f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,9 +27,9 @@ The MCP Server is the main entry point that communicates with AI agents via the ### 2. Inspector DLL (`WpfVisualTreeMcp.Inspector`) -The Inspector is a .NET Framework library that gets loaded into target WPF applications to perform inspection operations. +The Inspector is loaded into target WPF applications to perform inspection and interaction operations. -**Technology:** .NET Framework 4.8 (for maximum compatibility with WPF apps) +**Technology:** .NET Framework 4.7.2, .NET Framework 4.8, and .NET 8 for Windows **Responsibilities:** - Access `VisualTreeHelper` and `LogicalTreeHelper` @@ -48,7 +48,7 @@ The Inspector is a .NET Framework library that gets loaded into target WPF appli The Injector is responsible for loading the Inspector DLL into target WPF processes. -**Technology:** C++/CLI or managed code +**Technology:** .NET 8 managed injection logic, native C++ bootstrapper, and an x86 .NET 8 helper **Responsibilities:** - Enumerate running WPF processes @@ -170,18 +170,18 @@ public async Task GetVisualTreeAsync(ElementHandle root) ### Process Isolation - The MCP Server runs in a separate process from target applications -- Inspector DLL has read-only access to the visual tree -- No modification of application state is possible through inspection +- The Inspector runs in-process and can inspect the visual tree +- Interaction and live-property tools intentionally modify application state ### Named Pipe Security -- Pipes are created with appropriate ACLs -- Only the MCP Server process can connect -- Pipe names include process IDs to prevent collisions +- Pipes are local to the machine and their names include process IDs +- The protocol does not authenticate the connecting client ### Injection Safety -- Only .NET Framework WPF applications can be inspected -- Injection uses safe managed code techniques -- Target application stability is preserved +- .NET Framework and .NET 8 WPF applications are supported +- Auto-injection loads native and managed code by creating a remote thread +- .NET Framework private dependencies are resolved only for the Inspector payload chain in the Inspector directory +- Process permissions, endpoint security, runtime conflicts, or blocked UI threads can prevent injection or affect the target ## Error Handling diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index e28b781..de6fab8 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -21,17 +21,23 @@ Before you begin, ensure you have: cd WpfVisualTreeMcp ``` -2. Build the solution: - ```bash - dotnet build +2. For a complete Auto-injection publish, build both native bootstrappers and + publish the server: + ```powershell + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=x64 + msbuild src/WpfVisualTreeMcp.Bootstrapper/WpfVisualTreeMcp.Bootstrapper.vcxproj /m /p:Configuration=Release /p:Platform=Win32 + dotnet publish src/WpfVisualTreeMcp.Server/WpfVisualTreeMcp.Server.csproj -c Release -o ./publish ``` + The native steps require Visual Studio Build Tools with Desktop development + with C++. A managed `dotnet build` is sufficient when Auto-injection is not needed. + 3. The MCP server will be available at: ``` - src/WpfVisualTreeMcp.Server/bin/Debug/net8.0/WpfVisualTreeMcp.Server.exe + publish/WpfVisualTreeMcp.Server.exe ``` -### Option 2: .NET Tool Installation (Coming Soon) +### Option 2: .NET Tool Installation ```bash dotnet tool install -g WpfVisualTreeMcp @@ -48,7 +54,7 @@ There are multiple ways to configure the MCP server in Claude Code: Use the `claude mcp add` command for quick setup: ```bash -# Build the server first +# Build the server first (use the complete source-publish steps above for Auto-injection) cd WpfVisualTreeMcp dotnet build -c Release @@ -122,7 +128,7 @@ Restart Claude Code or reload the window after making changes. ## Setting Up Your WPF Application (Self-Hosted Mode) -For the MCP server to inspect your WPF application, you need to add the Inspector DLL to your project. This is called "self-hosted mode" and is the recommended approach. +For the MCP server to inspect your WPF application without runtime injection, add the Inspector DLL to your project. This is called "self-hosted mode." ### Step 1: Add Project Reference @@ -134,6 +140,9 @@ Add a reference to `WpfVisualTreeMcp.Inspector` in your WPF project: ``` +The Inspector supports WPF applications targeting .NET Framework 4.7.2, +.NET Framework 4.8, and .NET 8 for Windows. + ### Step 2: Initialize the Inspector In your `App.xaml.cs`, initialize the inspector on startup: @@ -175,7 +184,7 @@ Either start your own WPF application (with the Inspector set up as above) or us ```bash cd WpfVisualTreeMcp -dotnet run --project samples/SampleWpfApp +dotnet run --project samples/SampleWpfApp --framework net8.0-windows ``` The sample app already has the Inspector configured. @@ -255,7 +264,7 @@ What styles are defined in this application? ### "No WPF applications found" - Ensure the target application is running -- Check that it's a .NET Framework WPF application +- Check that it is a supported .NET Framework or .NET 8 WPF application - The application must have a main window visible ### "Failed to attach to process" or "Inspector not loaded" @@ -291,7 +300,8 @@ What styles are defined in this application? ## Next Steps -- Read the [Tools Reference](TOOLS_REFERENCE.md) for complete API documentation +- See the [current tool list](../README.md#available-tools) and the + [Tools Reference](TOOLS_REFERENCE.md) for detailed examples of the original tools - Explore the [Architecture](ARCHITECTURE.md) to understand how it works - Try the sample application with intentional binding errors - Integrate into your development workflow diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5ef3236..4f75844 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -24,10 +24,10 @@ the top priority. ## Priority 1 — Live tweak & measure -The headline theme. Today the agent can **read** properties (`wpf_get_element_properties`, -`wpf_watch_property`) but cannot **write** an arbitrary one. Adding live editing, paired -with a way to *measure* the effect, lets an agent answer "will this change work?" in -seconds instead of an edit-rebuild-relaunch cycle. +The headline theme. The agent can **read** properties (`wpf_get_element_properties`, +`wpf_watch_property`), **write** an arbitrary one, and measure the result. The shipped +live-editing and snapshot tools let an agent answer "will this change work?" in seconds +instead of an edit-rebuild-relaunch cycle. ### 1a. `wpf_set_property` — live property editing ✅ *(v0.9.0)* @@ -161,7 +161,8 @@ desktop work is going. Large because the visual-tree/injection specifics differ. 1. ~~**`wpf_set_property` + `wpf_revert_*`** (1a)~~ — ✅ shipped in v0.9.0. 2. ~~**`wpf_snapshot` + `wpf_diff`** (1b)~~ — ✅ shipped in v0.10.0. The "change → measure → is it effective?" loop is now fully automated (live-edit, then diff a before/after snapshot). -3. **`wpf_evaluate_binding`** (1c) and **`wpf_report`** (2b) — cheap diagnostics wins. +3. ~~**`wpf_evaluate_binding`** (1c)~~ — ✅ shipped in v0.11.0; **`wpf_report`** (2b) + is the next cheap diagnostics win. 4. **`wpf_record` → `wpf_export_test`** (2a) — the big competitive play; batch actions (backlog) as a prerequisite. 5. **Streaming** (3a) and **reach** (3b/3c) — larger architectural investments once the diff --git a/docs/TOOLS_REFERENCE.md b/docs/TOOLS_REFERENCE.md index 288e5b1..9cb4f41 100644 --- a/docs/TOOLS_REFERENCE.md +++ b/docs/TOOLS_REFERENCE.md @@ -1,6 +1,8 @@ # Tools Reference -Complete reference for all MCP tools provided by WpfVisualTreeMcp. +Detailed reference for the original inspection tools provided by +WpfVisualTreeMcp. For the current list of all 28 tools, see the +[README](../README.md#available-tools) or run `wpfinspect help`. ## Process Management @@ -418,7 +420,9 @@ Monitor a property for changes. } ``` -**Note:** Property changes are reported as MCP notifications. +**Note:** The watch is registered and returns its initial value, but the current +external transports do not forward later changes. Re-read the property to observe +updates; streaming notifications are planned. **Example Usage:** ``` diff --git a/samples/SampleWpfApp/App.xaml.cs b/samples/SampleWpfApp/App.xaml.cs index 0a34c3e..05bcef6 100644 --- a/samples/SampleWpfApp/App.xaml.cs +++ b/samples/SampleWpfApp/App.xaml.cs @@ -1,6 +1,5 @@ -using System.Diagnostics; +using System; using System.Windows; -using WpfVisualTreeMcp.Inspector; namespace SampleWpfApp; @@ -9,19 +8,38 @@ namespace SampleWpfApp; /// public partial class App : Application { +#if SELF_HOSTED_INSPECTOR + private bool _inspectorStarted; +#endif + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); - // Initialize the WPF Visual Tree Inspector - // This enables the MCP server to inspect this application - InspectorService.Initialize(Process.GetCurrentProcess().Id); +#if SELF_HOSTED_INSPECTOR + _inspectorStarted = !string.Equals( + Environment.GetEnvironmentVariable("WPF_VISUAL_TREE_MCP_SELF_HOSTED"), + "false", + StringComparison.OrdinalIgnoreCase); + + if (_inspectorStarted) + { + // Initialize the WPF Visual Tree Inspector + // This enables the MCP server to inspect this application + SelfHostedInspector.Start(); + } +#endif } protected override void OnExit(ExitEventArgs e) { - // Clean up the inspector service - InspectorService.Instance?.Dispose(); +#if SELF_HOSTED_INSPECTOR + if (_inspectorStarted) + { + // Clean up the inspector service + SelfHostedInspector.Stop(); + } +#endif base.OnExit(e); } diff --git a/samples/SampleWpfApp/MainViewModel.cs b/samples/SampleWpfApp/MainViewModel.cs index 6ccdc46..39857f9 100644 --- a/samples/SampleWpfApp/MainViewModel.cs +++ b/samples/SampleWpfApp/MainViewModel.cs @@ -22,12 +22,15 @@ public class MainViewModel : INotifyPropertyChanged public MainViewModel() { - Items = new ObservableCollection + Items = new ObservableCollection(); + for (var i = 1; i <= 40; i++) { - new ItemModel { Name = "Item 1", Description = "First sample item" }, - new ItemModel { Name = "Item 2", Description = "Second sample item" }, - new ItemModel { Name = "Item 3", Description = "Third sample item" } - }; + Items.Add(new ItemModel + { + Name = $"Item {i}", + Description = $"Sample item {i}" + }); + } SubmitCommand = new RelayCommand(Submit, CanSubmit); ClearCommand = new RelayCommand(Clear); diff --git a/samples/SampleWpfApp/MainWindow.xaml b/samples/SampleWpfApp/MainWindow.xaml index 2a0982a..353f191 100644 --- a/samples/SampleWpfApp/MainWindow.xaml +++ b/samples/SampleWpfApp/MainWindow.xaml @@ -86,6 +86,7 @@ + @@ -114,7 +115,24 @@ - + + + + + + + + + + + + + + +