Skip to content

mdgrs-mei/WinUIShell

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

402 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

WinUIShell

GitHub license PowerShell Gallery PowerShell Gallery

Scripting WinUI 3 with PowerShell.

Demo

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.

Installation

Install-PSResource -Name WinUIShell

Requirements

  • PowerShell 7.4 or newer
  • Windows 10/11 build 10.0.17763.0 or newer

Quick Start

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()

QuickStart

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.

How It Works

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
})

ProgressBar

Supported APIs

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 ref or out modifiers

WindowsAppSDK version

The current supported WindowsAppSDK version is 1.7.250310001.

Event Callback

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.

MainRunspaceAsyncUI (Default)

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 = $button

It 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.

MainRunspaceSyncUI

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.

RunspacePoolAsyncUI

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 $PSScriptRoot

Note 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 for awaitable types

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()

XamlReader

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()

Major Limitations

  • Data binding is not supported

Contributing

Code of Conduct

Please read our Code of Conduct to foster a welcoming environment. By participating in this project, you are expected to uphold this code.

Have a question or want to showcase something?

Please come to our Discussions page and avoid filing an issue to ask a question.

Want to file an issue or make a PR?

Please see our Contribution Guidelines.

Credits

WinUIShell uses:

About

Scripting WinUI 3 with PowerShell

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors 2

  •  
  •