Scripting WinUI 3 with PowerShell.
WinUIShell is a PowerShell module that allows you to create WinUI 3 applications in PowerShell.
Note
This module is in its prototyping phase. Frequent breaking changes are expected until this notice is removed.
Important
Starting from v0.10.0, the WinUIShell namespace structure follows the original namespace structure in the WindowsAppSDK. For example, Button class belongs to the WinUIShell.Microsoft.UI.Xaml.Controls namespace.
Install-PSResource -Name WinUIShell- PowerShell 7.4 or newer
- Windows 10/11 build 10.0.17763.0 or newer
This code creates a Window that has a clickable button:
using namespace WinUIShell.Microsoft.UI.Xaml
using namespace WinUIShell.Microsoft.UI.Xaml.Controls
Import-Module WinUIShell
$win = [Window]::new()
$win.Title = 'Hello from PowerShell!'
$win.AppWindow.ResizeClient(400, 200)
$button = [Button]::new()
$button.Content = 'Click Me'
$button.HorizontalAlignment = 'Center'
$button.VerticalAlignment = 'Center'
$button.AddClick({
$button.Content = 'Clicked!'
})
$win.Content = $button
# Activate() shows the window but does not block the script.
$win.Activate()
$win.WaitForClosed()If you dot-source the script and comment out $win.WaitForClosed(), you can inspect UI objects or even modify them on the terminal:
PS> $button
Flyout :
ClickMode : Release
CommandParameter :
IsPointerOver : False
IsPressed : False
Content : Click Me
ContentTemplate :
ContentTemplateRoot : WinUIShell.Microsoft.UI.Xaml.Controls.TextBlock
:Since the API of WinUIShell follows the WinUI 3's API, you can read the Windows App SDK documentation to see what should be available. The documentation of the Button class is here for example.
You can also refer to the examples folder for script samples.
WinUIShell launches a server process WinUIShell.Server.exe that provides all the UI functionalities. The WinUIShell module communicates with the server through IPC (Inter-Process Communication) to create UI elements and handle events. No WinUI 3 dlls are loaded in PowerShell.
This model simplifies the script structure. You can write long-running code in event handlers without blocking GUI. It's also allowed to access properties of UI elements directly on any thread without using Dispatchers.
This works:
$status = [TextBlock]::new()
$progressBar = [ProgressBar]::new()
$button.AddClick({
$button.IsEnabled = $false
$status.Text = 'Downloading...'
1..50 | ForEach-Object {
$progressBar.Value = $_
Start-Sleep -Milliseconds 50
}
$status.Text = 'Installing...'
51..100 | ForEach-Object {
$progressBar.Value = $_
Start-Sleep -Milliseconds 50
}
$status.Text = '🎉Done!'
$button.IsEnabled = $true
})All WinUI3 controls and types become accessible by adding "WinUIShell" as a prefix to their namespaces.
$button = [WinUIShell.Microsoft.UI.Xaml.Controls.Button]::new()Most methods and properties are automatically generated by the source generator, but those that use the following types are not supported:
- Arrays
- Delegates
- Pointers
- Types with
reforoutmodifiers
The current supported WindowsAppSDK version is 1.7.250310001.
Event callbacks are script blocks that are invoked when UI events are fired. You can register them with Add{EventName} methods of UI elements. The typical example is the Click event of a button:
$button = [Button]::new()
$argumentList = 1, 2
$button.AddClick({
param ($argumentList, $s, $e)
Write-Host "ArgumentList: $argumentList"
Write-Host "Sender: $s"
Write-Host "EventArgs: $e"
}, $argumentList)The same code can also be written using the EventCallback class. It allows you to customize the callback behavior:
$button = [Button]::new()
$clickCallback = [EventCallback]::new()
$clickCallback.RunspaceMode = 'RunspacePoolAsyncUI'
$clickCallback.DisabledControlsWhileProcessing = $button
$clickCallback.ArgumentList = 1, 2
$clickCallback.ScriptBlock = {
param ($argumentList, $s, $e)
Write-Host "ArgumentList: $argumentList"
Write-Host "Sender: $s"
Write-Host "EventArgs: $e"
}
$button.AddClick($clickCallback)The RunspaceMode property controls where and how the script block runs.
$clickCallback.RunspaceMode = 'RunspacePoolAsyncUI'There are three runspace modes, MainRunspaceAsyncUI, MainRunspaceSyncUI, and RunspacePoolAsyncUI.
The default value of RunspaceMode is MainRunspaceAsyncUI which means that the script block runs in the runspace where the script block is created (Main runspace). The script block can be a bound script block and sees the global or script scope variables in the runspace.
AsyncUI means that the callbacks do not block the UI thread on the server side. Even if a callback takes long time to finish, the UI stays responsive. If a button is pressed while the previous callback is running, the new callback is queued and processed after the previous one completes. If this behavior is not desirable, you can specify controls that are disabled on the server side while the event callback is running:
$clickCallback.DisabledControlsWhileProcessing = $buttonIt is a good practice to set the DisabledControlsWhileProcessing for long-running callbacks to avoid unintuitive queuing.
There is one callback queue per runspace where WinUIShell module is loaded, and callbacks in the queue are typically processed inside Window.WaitForClosed method or {AwaitableType}.WaitForCompleted method. Please see MultipleRunspaces.ps1 for an example of multi-runspace scenario.
Callbacks in MainRunspaceSyncUI mode run in the main runspace just like those with MainRunspaceAsyncUI, but they block the UI thread on the server side until they complete. Because they block the UI thread, it is guaranteed that no other events are triggered while the callback is running (No need to set DisabledControlsWhileProcessing).
Some callbacks need to run in MainRunspaceSyncUI mode to work properly. BeforeTextChanging event callback of the TextBox requires this mode for example. See TextBoxValidation.ps1 as a use case.
Callbacks in RunspacePoolAsyncUI mode are handled in parallel by multiple runspaces in the runspace pool. This mode is ideal for long-running callbacks that must keep other callbacks responsive during execution. You can specify the number of runspaces in the pool and the script block that defines the global variables and functions in the runspaces:
Set-WUIRunspacePoolOption -RunspaceCount 5 -InitializationScript {
param ($ScriptRoot)
$globalVar = 'Global variable in the runspace.'
function GlobalFunction() {
'Global function in the runspace.'
}
} -InitializationScriptArgumentList $PSScriptRootNote that the WinUIShell module is automatically loaded in each runspace.
Since the callbacks are executed in parallel, you should pass variables via ArgumentList and handle thread safety just as you would with Start-ThreadJob. See CancelLongRunningEventCallback.ps1 as a basic example, and MultipleProgressBars.ps1 as an example of multiple concurrent tasks.
WaitForCompleted method is added to all awaitable types, that is, types that implement GetAwaiter method, such as the Task class. By calling WaitForCompleted, you can wait for the task to complete while allowing the event handlers in that runspace to be processed.
$dialog = [ContentDialog]::new()
$dialog.XamlRoot = $rootElement.XamlRoot
$dialog.Title = 'Save your work?'
$dialog.PrimaryButtonText = 'Save'
# Returns [IAsyncOperation[ContentDialogResult]].
$asyncOperation = $dialog.ShowAsync()
# Returns [ContentDialogResult] when the dialog is closed. It processes other event handlers in the waiting loop if they exist.
$result = $asyncOperation.WaitForCompleted()Instead of creating UI elements by code, you can also create them by loading XAML similar to WPF in PowerShell. You can search for an UI element by the FindName method of FrameworkElement and add event handlers from PowerShell. Note that you can't use x:Class attributes or code-behind binding in XAML as mentioned in this documentation.
using namespace WinUIShell.Microsoft.UI.Xaml.Markup
$xamlString = @'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Button x:Name="button" Content="Click Me"/>
</StackPanel>
</Window>
'@
$win = [XamlReader]::Load($xamlString)
$win.AppWindow.ResizeClient(400, 200)
# FrameworkElement has FindName method (Window doesn't have it).
$button = $win.Content.FindName('button')
$button.AddClick({
$button.Content = 'Clicked!'
})
$win.Activate()
$win.WaitForClosed()- Data binding is not supported
Please read our Code of Conduct to foster a welcoming environment. By participating in this project, you are expected to uphold this code.
Please come to our Discussions page and avoid filing an issue to ask a question.
Please see our Contribution Guidelines.
WinUIShell uses:


