Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a384c6f
Add PowerShell-only WPF XAML designer foundation
papanda925 Sep 1, 2026
aad1174
Review 1: strengthen repository validation
papanda925 Sep 1, 2026
ef3f950
Review 2: harden XML parsing
papanda925 Sep 1, 2026
2e112f6
Review 3: make preview tree traversal robust
papanda925 Sep 1, 2026
66cb6e4
Review 4: protect unsaved editor changes
papanda925 Sep 1, 2026
87f7415
Review 5: validate PowerShell code-behind before save
papanda925 Sep 1, 2026
8b3be07
Review 6: add XAML undo and redo history
papanda925 Sep 1, 2026
c1bd2ed
Review 7: support nested layout drop targets
papanda925 Sep 1, 2026
18d914a
Review 8: add XAML document outline
papanda925 Sep 1, 2026
4a0d1cb
Review 9: make generated event code rename-safe
papanda925 Sep 1, 2026
3690cd0
Review 10: add real startup smoke test and fix UI wiring
papanda925 Sep 1, 2026
f64df38
Review 11: repair code-behind generation and parser failure
papanda925 Sep 1, 2026
cb3ae05
Review 12: protect paired PowerShell files during Save As
papanda925 Sep 1, 2026
305ae3d
Review 13: expose Grid and DockPanel attached properties
papanda925 Sep 1, 2026
9afa5d9
Review 14: add keyboard nudging for Canvas controls
papanda925 Sep 1, 2026
b901898
Review 15: make shortcuts editor-aware and wire arrow nudging
papanda925 Sep 1, 2026
eb618c5
Review 16: support nested drops into empty single-child containers
papanda925 Sep 1, 2026
92beae9
Review 17: ensure event handlers are registered before ShowDialog
papanda925 Sep 1, 2026
cc4a930
Review 17: insert generated handlers into the pre-ShowDialog event re…
papanda925 Sep 1, 2026
3ec19f5
Review 17: add a generated event region before ShowDialog
papanda925 Sep 1, 2026
14f040a
Review 17: validate event-region markers in the template
papanda925 Sep 1, 2026
f9130eb
Review 18: retain the preview Window for root property editing
papanda925 Sep 1, 2026
d42c6d0
Review 18: preserve root Window selection across preview refresh
papanda925 Sep 1, 2026
9cb7c40
Review 18: show the full XAML tree including the Window root
papanda925 Sep 1, 2026
667898b
Review 18: allow selecting the Window root from Outline
papanda925 Sep 1, 2026
a7a08eb
Review 19: add generated code regression test
papanda925 Sep 1, 2026
7a03061
Review 19: run generated-code regression in Windows CI
papanda925 Sep 1, 2026
e153821
Review 20: rewrite code-behind synchronization cleanly after final audit
papanda925 Sep 1, 2026
067a35d
Docs: align repository overview with completed designer features
papanda925 Sep 1, 2026
3e0f3b8
Docs: update usage, shortcuts, layout support, and roadmap
papanda925 Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/powershell-xaml-designer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: PowerShell XAML Designer checks

on:
push:
branches: [ main ]
pull_request:

jobs:
validate:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Parse PowerShell and XAML
shell: pwsh
run: .\Tests\Test-Repository.ps1
- name: Verify WPF assemblies can load
shell: pwsh
run: |
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName PresentationFramework
[System.Windows.Window] | Out-String | Write-Host

- name: Test generated PowerShell code
shell: pwsh
run: .\Tests\Test-CodeGeneration.ps1

- name: Smoke test designer startup in STA
shell: cmd
run: powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File .\Tests\Test-DesignerStartup.ps1
100 changes: 99 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,99 @@
# PowerShell_WPF_XAML_GUI_SampleCodes
# PowerShell WPF XAML GUI Sample Codes / PowerShell XAML Designer

This repository is being refocused around a **PowerShell-only WPF/XAML GUI designer** for Windows.

The goal is to make it possible to create and maintain PowerShell WPF screens even in environments where Visual Studio, Blend, paid IDE add-ons, or third-party GUI designers cannot be installed or used.

The original WPF sample scripts are kept as compatibility/reference samples. The main tool is now under [`XamlDesigner`](./XamlDesigner/).

## PowerShell XAML Designer

The designer follows a two-file model:

```text
MyWindow.xaml # UI / layout
MyWindow.ps1 # PowerShell control references, event registration, application logic
```

This is intentional. A standalone PowerShell `XamlReader` does not use the compiled C#/VB code-behind model that Visual Studio WPF projects use. Therefore the designer generates PowerShell event registration such as:

```powershell
[System.Windows.Controls.Button]$Button1 = $Window.FindName('Button1')

$Button1.Add_Click({
param($sender, $e)

# Add logic here.
})
```

instead of depending on `Click="Button1_Click"` in XAML.

### Current features

- PowerShell + WPF only; no Visual Studio and no third-party module required at runtime.
- New / Open / Save / Save As for paired `.xaml` and `.ps1` files.
- Runtime-discovered toolbox for public, instantiable WPF visual element types.
- Toolbox search and category filtering.
- Drag and drop from the toolbox to Canvas/Grid/StackPanel/DockPanel/WrapPanel/UniformGrid layouts and supported empty single-child containers such as Border and GroupBox.
- Direct mouse movement for controls whose parent is a `Canvas`.
- Arrow-key Canvas movement (1 px, or 10 px with Shift) plus optional 10-pixel mouse snap-to-grid.
- XAML document Outline, including selection/editing of the root Window.
- Selection of controls on the preview surface.
- Undo/redo for designer-side XAML changes, while text editors keep their native text undo/redo.
- Reflection-based property browser and property editing, including Canvas/Grid/DockPanel attached properties.
- Reflection-based event browser.
- Double-click an event to generate PowerShell event code.
- Double-click a control on the designer to generate its typical/default event (`Click`, `SelectionChanged`, `TextChanged`, etc.).
- XAML source editing, XML formatting, and well-formedness validation with line/position reporting.
- Live WPF preview through `System.Windows.Markup.XamlReader`.
- Preview-time removal of Visual Studio/Blend build-time attributes such as `x:Class`, `mc:Ignorable`, and `d:*`.
- Preview-time removal of XAML event attributes that cannot be resolved by standalone PowerShell, while preserving the source document.
- Automatic synchronization of named XAML controls into a generated control-reference region in the paired `.ps1` file.
- Generated event handlers are placed in a dedicated region before `$Window.ShowDialog()`, so handlers are registered before the UI runs.
- Existing user event code is not overwritten when control references are refreshed.
- Save As warns before replacing an already-existing paired `.ps1` file.

### Start the designer

Windows PowerShell 5.1:

```powershell
powershell.exe -STA -ExecutionPolicy Bypass -File .\XamlDesigner\Start-XamlDesigner.ps1
```

PowerShell 7 on Windows:

```powershell
pwsh.exe -STA -File .\XamlDesigner\Start-XamlDesigner.ps1
```

WPF requires Windows and an STA thread.

## Design direction

Visual Studio's XML editor provides features such as XML syntax checking, schema-aware validation, IntelliSense, snippets, and document outlining. The PowerShell XAML Designer aims to provide the most useful subset for standalone PowerShell/WPF work without requiring a Visual Studio project.

The visual designer intentionally uses a `Canvas` as the default root layout because absolute positioning makes drag/move behavior deterministic. Existing `Grid`, `StackPanel`, `DockPanel`, `WrapPanel`, and `UniformGrid` layouts can still be opened and previewed, and controls can be added to supported root layout containers. Direct coordinate dragging is currently limited to `Canvas` parents.

See [`XamlDesigner/README.md`](./XamlDesigner/README.md) for architecture, workflow, limitations, and planned editor features.

## Existing samples

The original files remain available at the repository root as WPF/PowerShell examples:

- `WPF_CustomGraphicalInputBoxSample.*`
- `WPF_GraphicalDatePickerSample.*`
- `WPF_InkCanvas.ps1`
- `WPF_OCR_Sample.ps1`
- `WPF_SimpleWeatherFormSample/`

They are useful as real-world XAML compatibility samples for the designer.

## Microsoft references

- Visual Studio XML editor: https://learn.microsoft.com/visualstudio/xml-tools/xml-editor
- Editing XML files: https://learn.microsoft.com/visualstudio/xml-tools/how-to-edit-xml-files
- XML editor IntelliSense: https://learn.microsoft.com/visualstudio/xml-tools/xml-editor-intellisense-features
- WPF drag and drop: https://learn.microsoft.com/dotnet/desktop/wpf/advanced/drag-and-drop-overview
- WPF dependency properties: https://learn.microsoft.com/dotnet/desktop/wpf/properties/dependency-properties-overview
83 changes: 83 additions & 0 deletions Tests/Test-CodeGeneration.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
[CmdletBinding()]
param()

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if ($env:OS -ne 'Windows_NT') {
Write-Host 'Code-generation test skipped: WPF requires Windows.'
return
}

Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName PresentationFramework

$repositoryRoot = Split-Path -Parent $PSScriptRoot
$designerDirectory = Join-Path $repositoryRoot 'XamlDesigner'
$coreDirectory = Join-Path $designerDirectory 'Core'

. (Join-Path $coreDirectory 'State.ps1')
. (Join-Path $coreDirectory 'Xml.ps1')
. (Join-Path $coreDirectory 'CodeBehind.ps1')
. (Join-Path $coreDirectory 'Events.ps1')

$script:State.BaseDirectory = $designerDirectory
$script:State.ToolboxItems = @()
$script:State.XamlDocument = New-XmlDocumentFromText -Text @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="MainWindow">
<Canvas x:Name="DesignCanvas">
<Button x:Name="Button1" Content="Test"/>
</Canvas>
</Window>
'@

$referenceLines = @(Get-ControlReferenceLines)
$expectedReference = '[System.Windows.Controls.Button]${Button1} = $Window.FindName(''Button1'')'
if ($referenceLines -notcontains $expectedReference) {
throw "Expected generated control reference was not found. Actual: $($referenceLines -join '; ')"
}

$code = Get-Content -LiteralPath (Join-Path $designerDirectory 'Templates\BlankWindow.ps1') -Raw
$script:State.Ui = @{
CodeEditor = [pscustomobject]@{ Text = $code }
MainTabs = [pscustomobject]@{ SelectedIndex = 0 }
StatusText = [pscustomobject]@{ Text = '' }
}
$script:State.CurrentXamlPath = 'C:\Temp\GeneratedWindow.xaml'
$script:State.SelectedElementName = 'Button1'
$script:State.SelectedRuntimeElement = [System.Windows.Controls.Button]::new()
$script:State.SelectedRuntimeElement.Name = 'Button1'

Sync-CodeEditor
Generate-EventHandlerForName -EventName 'Click'
$generated = $script:State.Ui.CodeEditor.Text

$eventMarker = '# <XamlDesigner:Event Control="Button1" Name="Click">'
if (-not $generated.Contains($eventMarker)) {
throw 'Generated Click event marker was not found.'
}

$handlerIndex = $generated.IndexOf('${Button1}.Add_Click(', [System.StringComparison]::Ordinal)
$showDialogIndex = $generated.IndexOf('$null = $Window.ShowDialog()', [System.StringComparison]::Ordinal)
if ($handlerIndex -lt 0 -or $showDialogIndex -lt 0 -or $handlerIndex -gt $showDialogIndex) {
throw 'Generated event handler must be registered before $Window.ShowDialog().'
}

$tokens = $null
$parseErrors = $null
[void][System.Management.Automation.Language.Parser]::ParseInput(
$generated,
[ref]$tokens,
[ref]$parseErrors
)
if ($parseErrors.Count -gt 0) {
$messages = $parseErrors | ForEach-Object {
"line $($_.Extent.StartLineNumber): $($_.Message)"
}
throw "Generated PowerShell does not parse: $($messages -join '; ')"
}

Write-Host 'Generated control references and event registration order are valid.'
57 changes: 57 additions & 0 deletions Tests/Test-DesignerStartup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[CmdletBinding()]
param()

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

if ($env:OS -ne 'Windows_NT') {
Write-Host 'Designer startup smoke test skipped: WPF requires Windows.'
return
}

if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne [System.Threading.ApartmentState]::STA) {
throw 'Designer startup smoke test must run in an STA PowerShell process.'
}

Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName PresentationFramework

$repositoryRoot = Split-Path -Parent $PSScriptRoot
$designerDirectory = Join-Path $repositoryRoot 'XamlDesigner'
Import-Module (Join-Path $designerDirectory 'XamlDesigner.Core.psm1') -Force

$designerXamlPath = Join-Path $designerDirectory 'XamlDesigner.xaml'
[xml]$designerXaml = Get-Content -LiteralPath $designerXamlPath -Raw
$reader = [System.Xml.XmlNodeReader]::new($designerXaml)
try {
[System.Windows.Window]$window = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}

Initialize-XamlDesigner -Window $window -BaseDirectory $designerDirectory

$requiredControls = @(
'MenuNew','MenuOpen','MenuSave','MenuSaveAs','MenuExit',
'MenuUndo','MenuRedo','MenuDelete','MenuDuplicate','MenuValidate','MenuRefreshToolbox','MenuAbout',
'StatusText','DocumentText','ToolboxSearch','ToolboxCategory','ToolboxList','OutlineTree',
'MainTabs','PreviewBorder','PreviewHost','CheckSnapToGrid',
'ButtonDelete','ButtonDuplicate','ButtonApplyXaml','ButtonFormatXaml','ButtonValidateCode',
'XamlEditor','CodeEditor','SelectedControlText','PropertyGrid',
'PropertyNameText','PropertyValueText','ButtonApplyProperty','EventGrid'
)

foreach ($name in $requiredControls) {
if ($null -eq $window.FindName($name)) {
throw "Designer UI smoke test failed: '$name' was not found."
}
}

if ($window.Title -notlike 'PowerShell XAML Designer*') {
throw "Designer initialization did not set the expected window title: $($window.Title)"
}

$window.Close()
Write-Host 'PowerShell XAML Designer startup smoke test passed.'
74 changes: 74 additions & 0 deletions Tests/Test-Repository.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[CmdletBinding()]
param()

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repositoryRoot = Split-Path -Parent $PSScriptRoot
$errorsFound = [System.Collections.Generic.List[string]]::new()

Get-ChildItem -LiteralPath $repositoryRoot -Recurse -File -Include *.ps1,*.psm1 | ForEach-Object {
$tokens = $null
$parseErrors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors)
foreach ($parseError in $parseErrors) {
$errorsFound.Add("PowerShell parse error: $($_.FullName):$($parseError.Extent.StartLineNumber):$($parseError.Extent.StartColumnNumber) $($parseError.Message)")
}
}

Get-ChildItem -LiteralPath $repositoryRoot -Recurse -File -Include *.xaml | ForEach-Object {
try {
$settings = [System.Xml.XmlReaderSettings]::new()
$settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
$settings.XmlResolver = $null
$reader = [System.Xml.XmlReader]::Create($_.FullName, $settings)
try {
$document = [System.Xml.XmlDocument]::new()
$document.XmlResolver = $null
$document.Load($reader)
}
finally {
$reader.Close()
}
}
catch {
$errorsFound.Add("XML parse error: $($_.FullName): $($_.Exception.Message)")
}
}

$requiredFiles = @(
'XamlDesigner\Start-XamlDesigner.ps1',
'XamlDesigner\XamlDesigner.xaml',
'XamlDesigner\XamlDesigner.Core.psm1',
'XamlDesigner\Templates\BlankWindow.xaml',
'XamlDesigner\Templates\BlankWindow.ps1'
)
foreach ($relativePath in $requiredFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $repositoryRoot $relativePath))) {
$errorsFound.Add("Required designer file is missing: $relativePath")
}
}

$templateCodePath = Join-Path $repositoryRoot 'XamlDesigner\Templates\BlankWindow.ps1'
if (Test-Path -LiteralPath $templateCodePath) {
$templateCode = Get-Content -LiteralPath $templateCodePath -Raw
foreach ($marker in @(
'# <XamlDesigner:XamlFile>',
'# </XamlDesigner:XamlFile>',
'# <XamlDesigner:ControlReferences>',
'# </XamlDesigner:ControlReferences>',
'# <XamlDesigner:Events>',
'# </XamlDesigner:Events>'
)) {
if (-not $templateCode.Contains($marker)) {
$errorsFound.Add("Generated-code marker is missing from BlankWindow.ps1: $marker")
}
}
}

if ($errorsFound.Count -gt 0) {
$errorsFound | ForEach-Object { Write-Error $_ }
throw "$($errorsFound.Count) repository validation error(s) found."
}

Write-Host 'PowerShell syntax, XAML/XML well-formedness, required files, and generated-code marker checks passed.'
Loading
Loading