Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
917334d
Persona reviews 1-10: improve beginner and accessible designer UI
papanda925 Sep 1, 2026
f84e3f0
Persona reviews 1-10: add common-control mode and beginner descriptions
papanda925 Sep 1, 2026
da5e601
Persona reviews 1-20: improve insertion, defaults, and safe subtree d…
papanda925 Sep 1, 2026
07d9bde
Persona reviews 11-20: make history capable of restoring XAML and cod…
papanda925 Sep 1, 2026
d908176
Persona reviews 11-20: restore XAML and generated code as one undo unit
papanda925 Sep 1, 2026
bc5fbfd
Persona reviews 11-20: make generated event lifecycle beginner-safe
papanda925 Sep 1, 2026
63e046d
Persona reviews 11-20: add property guidance and consistent rollback
papanda925 Sep 1, 2026
7d487b2
Persona reviews 1-20: wire beginner onboarding and keyboard-friendly …
papanda925 Sep 1, 2026
1acd333
Persona reviews 21-30: harden encoding, paired-file safety, and save …
papanda925 Sep 1, 2026
45423fd
Persona reviews 21-30: add safe XAML preview rules and namescope-awar…
papanda925 Sep 1, 2026
d10529f
Persona reviews 21-30: preserve last good preview and release old WPF…
papanda925 Sep 1, 2026
bc63798
Persona reviews 21-30: auto-relaunch STA and improve startup diagnostics
papanda925 Sep 1, 2026
0470daa
Persona reviews 31-40: keep subtree delete and duplicate namescope-safe
papanda925 Sep 1, 2026
85d466a
Persona reviews 31-40: add core regression tests for beginner and ent…
papanda925 Sep 1, 2026
d8a6137
Persona reviews 31-40: adapt generated-code test to unified history
papanda925 Sep 1, 2026
468def6
Persona reviews 31-40: validate new beginner UI controls at startup
papanda925 Sep 1, 2026
a275c6f
Persona reviews 31-40: test PowerShell 5.1 and 7 in Windows CI
papanda925 Sep 1, 2026
07bbbb5
Persona review 41: recognize standard WPF presentation options namespace
papanda925 Sep 1, 2026
15b8b76
Persona review 41: allow safe standard presentation-options attributes
papanda925 Sep 1, 2026
7f2673d
Persona reviews 41-50: archive deleted-control event code instead of …
papanda925 Sep 1, 2026
07913a9
Persona reviews 41-50: preserve event code when controls are deleted
papanda925 Sep 1, 2026
964efb1
Persona reviews 41-50: verify deleted event code is archived safely
papanda925 Sep 1, 2026
19281cb
Persona reviews 41-50: add a Japanese beginner getting-started guide
papanda925 Sep 1, 2026
1106204
Persona reviews 41-50: document all 50 simulated reviewer findings an…
papanda925 Sep 1, 2026
8c57c17
Persona reviews 41-50: update top-level usage and safety documentation
papanda925 Sep 1, 2026
9a6fe71
Persona reviews 41-50: document beginner workflow, safe preview, and …
papanda925 Sep 1, 2026
f014421
Persona reviews 41-50: make repository checks enforce beginner docs a…
papanda925 Sep 1, 2026
6693526
Final persona audit: rebuild event generation and archive logic cleanly
papanda925 Sep 1, 2026
cb016fb
Fix Windows PowerShell 5.1 strict-mode XAML test reporting
papanda925 Sep 1, 2026
9024696
Normalize repository file filtering across PowerShell 5.1 and 7
papanda925 Sep 1, 2026
3421e53
Add UTF-8 BOM for Windows PowerShell 5.1 Japanese source compatibility
papanda925 Sep 1, 2026
9be0c83
Add UTF-8 BOM for Windows PowerShell 5.1 Japanese source compatibility
papanda925 Sep 1, 2026
a279d88
Fix archived-event regression regex
papanda925 Sep 1, 2026
e08271d
Correct .NET regex escaping in archived-event core test
papanda925 Sep 1, 2026
fa9b52c
Resolve WPF element types through loaded assembly for PowerShell 5.1
papanda925 Sep 1, 2026
2d77b19
Make generated runtime XAML event cleanup PowerShell 5.1 compatible
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
52 changes: 33 additions & 19 deletions .github/workflows/powershell-xaml-designer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,36 @@ 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
- uses: actions/checkout@v5

- name: Parse repository with PowerShell 7
shell: cmd
run: pwsh.exe -NoProfile -STA -File .\Tests\Test-Repository.ps1

- name: Parse repository with Windows PowerShell 5.1
shell: cmd
run: powershell.exe -NoProfile -STA -File .\Tests\Test-Repository.ps1

- name: Core behavior tests with PowerShell 7
shell: cmd
run: pwsh.exe -NoProfile -STA -File .\Tests\Test-DesignerCore.ps1

- name: Core behavior tests with Windows PowerShell 5.1
shell: cmd
run: powershell.exe -NoProfile -STA -File .\Tests\Test-DesignerCore.ps1

- name: Generated code tests with PowerShell 7
shell: cmd
run: pwsh.exe -NoProfile -STA -File .\Tests\Test-CodeGeneration.ps1

- name: Generated code tests with Windows PowerShell 5.1
shell: cmd
run: powershell.exe -NoProfile -STA -File .\Tests\Test-CodeGeneration.ps1

- name: Designer startup smoke test with PowerShell 7
shell: cmd
run: pwsh.exe -NoProfile -STA -File .\Tests\Test-DesignerStartup.ps1

- name: Designer startup smoke test with Windows PowerShell 5.1
shell: cmd
run: powershell.exe -NoProfile -STA -File .\Tests\Test-DesignerStartup.ps1
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ instead of depending on `Click="Button1_Click"` in XAML.
- 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.
- Toolbox search plus a beginner-oriented Common category with short control descriptions.
- Drag-and-drop, double-click, or an explicit Add selected control button for inserting controls.
- 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.
- Undo/redo for designer-side changes restores XAML and generated PowerShell together, while text editors keep 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.
Expand All @@ -52,23 +53,28 @@ instead of depending on `Click="Button1_Click"` in XAML.
- 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.
- Deleting controls archives their generated event handlers as comments instead of leaving broken references or silently discarding the code.
- Safe preview blocks risky loose-XAML constructs/custom CLR elements and prevents automatic external resource navigation.
- Save As warns before replacing an already-existing paired `.ps1` file; paired saves use temporary files/rollback and UTF-8 BOM for Windows PowerShell 5.1/Japanese compatibility.

### Start the designer

Windows PowerShell 5.1:
From a PowerShell prompt:

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

PowerShell 7 on Windows:
If the current process is not STA, the launcher attempts to relaunch the same PowerShell executable in STA mode automatically. It does not bypass the machine's execution policy.

```powershell
pwsh.exe -STA -File .\XamlDesigner\Start-XamlDesigner.ps1
```
Explicit launch examples:

~~~powershell
powershell.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1
pwsh.exe -NoProfile -STA -File .\XamlDesigner\Start-XamlDesigner.ps1
~~~

WPF requires Windows and an STA thread.
For a Japanese beginner walkthrough, see [XamlDesigner/GETTING_STARTED.ja.md](./XamlDesigner/GETTING_STARTED.ja.md). The requested 50 simulated persona reviews are recorded in [XamlDesigner/REVIEW_50_PERSONAS.md](./XamlDesigner/REVIEW_50_PERSONAS.md).

## Design direction

Expand Down
3 changes: 2 additions & 1 deletion Tests/Test-CodeGeneration.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ $coreDirectory = Join-Path $designerDirectory 'Core'
. (Join-Path $coreDirectory 'State.ps1')
. (Join-Path $coreDirectory 'Xml.ps1')
. (Join-Path $coreDirectory 'CodeBehind.ps1')
. (Join-Path $coreDirectory 'History.ps1')
. (Join-Path $coreDirectory 'Events.ps1')

$script:State.BaseDirectory = $designerDirectory
Expand All @@ -40,7 +41,7 @@ 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
$code = [System.IO.File]::ReadAllText((Join-Path $designerDirectory 'Templates\BlankWindow.ps1'))
$script:State.Ui = @{
CodeEditor = [pscustomobject]@{ Text = $code }
MainTabs = [pscustomobject]@{ SelectedIndex = 0 }
Expand Down
201 changes: 201 additions & 0 deletions Tests/Test-DesignerCore.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
[CmdletBinding()]
param()

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

if ($env:OS -ne 'Windows_NT') {
Write-Host 'Designer core behavior 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'

foreach ($part in @(
'State.ps1',
'Xml.ps1',
'CodeBehind.ps1',
'Documents.ps1',
'History.ps1',
'ToolboxCatalog.ps1',
'ToolboxEditing.ps1',
'Events.ps1'
)) {
. (Join-Path $coreDirectory $part)
}

$script:State.BaseDirectory = $designerDirectory
$script:State.ToolboxItems = @(Get-WpfControlCatalog)

$safeDocument = New-XmlDocumentFromText -Text @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Unused"
x:Name="MainWindow">
<Grid/>
</Window>
'@
if (-not (Test-XamlPreviewSafety -Document $safeDocument)) {
throw 'Safe standard XAML was unexpectedly rejected.'
}

$dangerousDocument = New-XmlDocumentFromText -Text @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<ObjectDataProvider x:Key="Danger"/>
</Window.Resources>
<Grid/>
</Window>
'@
$blocked = $false
try {
[void](Test-XamlPreviewSafety -Document $dangerousDocument)
}
catch {
$blocked = $true
}
if (-not $blocked) {
throw 'ObjectDataProvider was not blocked by safe preview.'
}

$customDocument = New-XmlDocumentFromText -Text @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Unknown">
<local:UnknownControl/>
</Window>
'@
$blocked = $false
try {
[void](Test-XamlPreviewSafety -Document $customDocument)
}
catch {
$blocked = $true
}
if (-not $blocked) {
throw 'Custom CLR element was not blocked by safe preview.'
}

$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">
<Canvas.Resources>
<DataTemplate x:Key="Template1">
<TextBox x:Name="TemplateTextBox"/>
</DataTemplate>
</Canvas.Resources>
<Button x:Name="Button1"/>
</Canvas>
</Window>
'@
$named = @(Get-AllNamedXamlElements | ForEach-Object Name)
if ($named -notcontains 'Button1' -or $named -notcontains 'DesignCanvas') {
throw 'Normal Window namescope controls were not discovered.'
}
if ($named -contains 'TemplateTextBox') {
throw 'Template NameScope control was incorrectly treated as a Window.FindName control.'
}

$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">
<GroupBox x:Name="Group1">
<StackPanel x:Name="Stack1">
<TextBox x:Name="Input1"/>
<TextBlock x:Name="Output1" Text="{Binding ElementName=Input1, Path=Text}"/>
</StackPanel>
</GroupBox>
</Canvas>
</Window>
'@
$source = Get-XamlElementByName -Name 'Group1'
$newRootName = Copy-XamlElementNode -Source $source
if ([string]::IsNullOrWhiteSpace($newRootName)) {
throw 'Subtree duplication did not return a new root name.'
}
$normalNames = @(Get-AllNamedXamlElements | ForEach-Object Name)
if (($normalNames | Select-Object -Unique).Count -ne $normalNames.Count) {
throw 'Subtree duplication created duplicate x:Name values.'
}
$copyNode = Get-XamlElementByName -Name $newRootName
$boundText = @($copyNode.SelectNodes('.//*') | Where-Object {
$_ -is [System.Xml.XmlElement] -and $_.HasAttribute('Text') -and $_.GetAttribute('Text') -like '*ElementName=*'
} | Select-Object -First 1)
if ($boundText.Count -ne 1 -or $boundText[0].GetAttribute('Text') -like '*ElementName=Input1,*') {
throw 'ElementName binding inside the duplicated subtree was not updated.'
}

$sampleCode = @'
# <XamlDesigner:Events>
# <XamlDesigner:Event Control="Button1" Name="Click">
${Button1}.Add_Click({
param($sender, $e)
})
# </XamlDesigner:Event>
# </XamlDesigner:Events>

Write-Host "user code"
'@
$archived = Archive-GeneratedEventsForControl -Code $sampleCode -ControlName 'Button1'
if ($archived -notmatch 'XamlDesigner:ArchivedEvent') {
throw 'Deleted-control event block was not archived.'
}
if ($archived -match '(?m)^\s*\$\{Button1\}\.Add_Click\(') {
throw 'Archived event handler is still executable.'
}
if ($archived -notmatch 'param\(\$sender, \$e\)') {
throw 'Archived event handler body was not preserved as comments.'
}
if ($archived -notmatch 'Write-Host "user code"') {
throw 'User code outside generated blocks was removed unexpectedly.'
}

$tempDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PowerShellXamlDesignerTest_' + [Guid]::NewGuid().ToString('N'))
[void][System.IO.Directory]::CreateDirectory($tempDirectory)
try {
$xamlPath = Join-Path $tempDirectory '日本語.xaml'
$codePath = Join-Path $tempDirectory '日本語.ps1'
$xamlText = '<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="こんにちは"><Grid/></Window>'
$codeText = 'Write-Output "こんにちは PowerShell"'

Write-DesignerDocumentPair -XamlPath $xamlPath -XamlText $xamlText -CodePath $codePath -CodeText $codeText

foreach ($path in @($xamlPath, $codePath)) {
[byte[]]$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -lt 3 -or $bytes[0] -ne 0xEF -or $bytes[1] -ne 0xBB -or $bytes[2] -ne 0xBF) {
throw "UTF-8 BOM was not written: $path"
}
}

if ((Read-DesignerTextFile -Path $xamlPath) -cne $xamlText) {
throw 'Japanese XAML did not round-trip exactly.'
}
if ((Read-DesignerTextFile -Path $codePath) -cne $codeText) {
throw 'Japanese PowerShell did not round-trip exactly.'
}
}
finally {
Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue
}

$script:State.Ui = @{
CodeEditor = [pscustomobject]@{ Text = 'Write-Host old' }
}
$script:State.SelectedElementName = 'Button1'
$snapshot = New-DesignerHistorySnapshot -XamlText '<Window />' -CodeText 'Write-Host old' -SelectionName 'Button1'
if ($snapshot.CodeText -ne 'Write-Host old' -or $snapshot.SelectionName -ne 'Button1') {
throw 'Designer history snapshot did not retain code/selection state.'
}

Write-Host 'Safe preview, NameScope, duplication, event archiving, encoding, and history behavior checks passed.'
8 changes: 4 additions & 4 deletions Tests/Test-DesignerStartup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ $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
[xml]$designerXaml = [System.IO.File]::ReadAllText($designerXamlPath)
$reader = [System.Xml.XmlNodeReader]::new($designerXaml)
try {
[System.Windows.Window]$window = [System.Windows.Markup.XamlReader]::Load($reader)
Expand All @@ -35,12 +35,12 @@ 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',
'MenuUndo','MenuRedo','MenuDelete','MenuDuplicate','MenuValidate','MenuRefreshToolbox','MenuGettingStarted','MenuAbout',
'StatusText','DocumentText','ToolboxSearch','ToolboxCategory','ToolboxList','ButtonAddToolbox','ToolboxHelpText','OutlineTree',
'MainTabs','PreviewBorder','PreviewHost','CheckSnapToGrid',
'ButtonDelete','ButtonDuplicate','ButtonApplyXaml','ButtonFormatXaml','ButtonValidateCode',
'XamlEditor','CodeEditor','SelectedControlText','PropertyGrid',
'PropertyNameText','PropertyValueText','ButtonApplyProperty','EventGrid'
'PropertyNameText','PropertyValueText','PropertyHelpText','ButtonApplyProperty','EventGrid'
)

foreach ($name in $requiredControls) {
Expand Down
Loading