Search code examples
c#powershellxamlwinui-3powershell-7.4

How to create WinUI3 GUI in PowerShell?


The Goal

Creating and rendering a simple WinUI3 GUI in PowerShell 7.5 which is based on .NET 9. Nothing complicated, just a window and a button in it, such as this XAML

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="App1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    </StackPanel>
</Window>

  • Using, importing or defining C# code in PowerShell is totally okay too for the solution.
  • The only thing I don't want to do is to build/compile binaries of my own such as executables or DLLs. I want to use .cs CSharp files uncompiled in PowerShell if there is a need for CSharp code.
  • Loading and using Microsoft-signed DLLs is completely okay.

What I've tried so far

I've created a fully working WinUI3 app in Visual Studio 2022 using the latest WindowsApps SDK. Then inside of the Winui3 project\App1\bin\x64\Debug\net8.0-windows10.0.22621.0\win-x64 folder I've tried loading all of the DLLs in there in PowerShell. Some 200 dlls loaded and a few failed to load.

In PowerShell now I have access to the type [Microsoft.UI.Xaml.Window] but when I try to create an instance of it

New-Object -TypeName Microsoft.UI.Xaml.Window
# Or
[Microsoft.UI.Xaml.Window]::new()

I get the following error

MethodInvocationException: Exception calling ".ctor" with "0" argument(s): "The type initializer for '_IWindowFactory' threw an exception."

It looks like there is a dependency missing for _IWindowFactory.


This is the full error message


Exception             : 
    Type           : System.Management.Automation.MethodInvocationException
    ErrorRecord    : 
        Exception             : 
            Type    : System.Management.Automation.ParentContainsErrorRecordException
            Message : Exception calling ".ctor" with "0" argument(s): "The type initializer for '_IWindowFactory' threw an exception."
            HResult : -2146233087
        CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException
        FullyQualifiedErrorId : TypeInitializationException
        InvocationInfo        : 
            ScriptLineNumber : 1
            OffsetInLine     : 1
            HistoryId        : 4
            Line             : [Microsoft.UI.Xaml.Window]::new()
            Statement        : [Microsoft.UI.Xaml.Window]::new()
            PositionMessage  : At line:1 char:1
                               + [Microsoft.UI.Xaml.Window]::new()
                               + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            CommandOrigin    : Internal
        ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1
    TargetSite     : 
        Name          : ConvertToMethodInvocationException
        DeclaringType : [System.Management.Automation.ExceptionHandlingOps]
        MemberType    : Method
        Module        : System.Management.Automation.dll
    Message        : Exception calling ".ctor" with "0" argument(s): "The type initializer for '_IWindowFactory' threw an exception."
    Data           : System.Collections.ListDictionaryInternal
    InnerException : 
        Type           : System.TypeInitializationException
        TypeName       : _IWindowFactory
        TargetSite     : 
            Name          : get_Instance
            DeclaringType : [Microsoft.UI.Xaml.Window+_IWindowFactory]
            MemberType    : Method
            Module        : Microsoft.WinUI.dll
        Message        : The type initializer for '_IWindowFactory' threw an exception.
        InnerException : 
            Type           : System.TypeInitializationException
            TypeName       : WinRT.ActivationFactory`1
            TargetSite     : 
                Name          : As
                DeclaringType : [WinRT.ActivationFactory`1[T]]
                MemberType    : Method
                Module        : Microsoft.WinUI.dll
            Message        : The type initializer for 'WinRT.ActivationFactory`1' threw an exception.
            InnerException : 
                Type       : System.Runtime.InteropServices.COMException
                ErrorCode  : -2147221164
                TargetSite : 
                    Name          : ThrowExceptionForHR
                    DeclaringType : [System.Runtime.InteropServices.Marshal]
                    MemberType    : Method
                    Module        : System.Private.CoreLib.dll
                Message    : Class not registered (0x80040154 (REGDB_E_CLASSNOTREG))
                Source     : System.Private.CoreLib
                HResult    : -2147221164
                StackTrace : 
   at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)
   at WinRT.BaseActivationFactory..ctor(String typeNamespace, String typeFullName)
   at WinRT.ActivationFactory`1..ctor()
   at WinRT.ActivationFactory`1..cctor()
            Source         : Microsoft.WinUI
            HResult        : -2146233036
            StackTrace     : 
   at WinRT.ActivationFactory`1.As(Guid iid)
   at Microsoft.UI.Xaml.Window._IWindowFactory..ctor()
   at Microsoft.UI.Xaml.Window._IWindowFactory..cctor()
        Source         : Microsoft.WinUI
        HResult        : -2146233036
        StackTrace     : 
   at Microsoft.UI.Xaml.Window._IWindowFactory.get_Instance()
   at Microsoft.UI.Xaml.Window..ctor()
   at CallSite.Target(Closure, CallSite, Type)
    Source         : System.Management.Automation
    HResult        : -2146233087
    StackTrace     : 
   at System.Management.Automation.ExceptionHandlingOps.ConvertToMethodInvocationException(Exception exception, Type typeToThrow, String methodName, Int32 numArgs, MemberInfo memberInfo)
   at CallSite.Target(Closure, CallSite, Type)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at System.Management.Automation.Interpreter.DynamicInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
FullyQualifiedErrorId : TypeInitializationException
InvocationInfo        : 
    ScriptLineNumber : 1
    OffsetInLine     : 1
    HistoryId        : 4
    Line             : [Microsoft.UI.Xaml.Window]::new()
    Statement        : [Microsoft.UI.Xaml.Window]::new()
    PositionMessage  : At line:1 char:1
                       + [Microsoft.UI.Xaml.Window]::new()
                       + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1


Other people have tried this too and had similar results. Another issue related to this problem asking for some guidance from Microsoft.

I don't know how Visual Studio does this that makes it all so easy and automated, but I believe I need to do the same tasks manually in PowerShell.


Solution

  • Simon provided a fantastic answer. I tried to convert his answer to an all powershell solution. The only part I couldn't figure out was why this line can't be translated one for one. $this.Resources.MergedDictionaries.Add([Microsoft.UI.Xaml.Controls.XamlControlsResources]::new())

    This builds the application in another runspace while leaving room to add to the DataContext and to call Window.Activate() when needed by dispatcherqueue.

    # cd 'C:\change\this'
    Add-Type -Path ".\WinRT.Runtime.dll"
    Add-Type -Path ".\Microsoft.Windows.SDK.NET.dll"
    Add-Type -Path ".\Microsoft.WindowsAppRuntime.Bootstrap.Net.dll"
    Add-Type -Path ".\Microsoft.InteractiveExperiences.Projection.dll"
    Add-Type -Path ".\Microsoft.WinUI.dll"
    
    # //Setup runspacepool and shared variable
    $ConcurrentDict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
    $State = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $RunspaceVariable = [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('ConcurrentDict', $ConcurrentDict, $null)
    $State.Variables.Add($RunspaceVariable)
    $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $([int]$env:NUMBER_OF_PROCESSORS + 1), $State, (Get-Host))
    $RunspacePool.Open()
    $Powershell = [PowerShell]::Create()
    $Powershell.RunspacePool = $RunspacePool
    
    $AppSetup = @'
    # cd 'C:\change\this'
    Add-Type -Path ".\WinRT.Runtime.dll"
    Add-Type -Path ".\Microsoft.Windows.SDK.NET.dll"
    Add-Type -Path ".\Microsoft.WindowsAppRuntime.Bootstrap.Net.dll"
    Add-Type -Path ".\Microsoft.InteractiveExperiences.Projection.dll"
    Add-Type -Path ".\Microsoft.WinUI.dll"
    
    class PwshWinUIApp : Microsoft.UI.Xaml.Application, Microsoft.UI.Xaml.Markup.IXamlMetadataProvider {
        # //App is able to load without Microsoft.UI.Xaml.Markup.IXamlMetadataProvider but interaction such as clicking a button will crash the terminal without it.
    
        $MainWindow
        $provider = [Microsoft.UI.Xaml.XamlTypeInfo.XamlControlsXamlMetaDataProvider]::new()
        static [bool]$OkWasClicked
        $SharedConcurrentDictionary
    
        [Microsoft.UI.Xaml.Markup.IXamlType]GetXamlType([type]$type) {
            return $this.provider.GetXamlType($type)
        }
        [Microsoft.UI.Xaml.Markup.IXamlType]GetXamlType([string]$fullname) {
            return $this.provider.GetXamlType($fullname)
        }
        [Microsoft.UI.Xaml.Markup.XmlnsDefinition[]]GetXmlnsDefinitions() {
            return $this.provider.GetXmlnsDefinitions()
        }
    
        PwshWinUIApp() {}
        PwshWinUIApp($SharedConcurrentDictionary) {
            $this.SharedConcurrentDictionary = $SharedConcurrentDictionary
        }
        OnLaunched([Microsoft.UI.Xaml.LaunchActivatedEventArgs]$a) {
            if ($null -ne $this.MainWindow) { return }
    
            # //Don't know why this line is problematic or how to get it to work in powershell. But the app works without it.
            # $this.Resources.MergedDictionaries.Add([Microsoft.UI.Xaml.Controls.XamlControlsResources]::new())
            
            $xaml = '<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
                    <StackPanel
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Orientation="Horizontal">
                        <TextBlock Text="{Binding tbContent, Mode=TwoWay}" Margin="10" />
                        <Button x:Name="okButton" Margin="10">OK</Button>
                        <Button x:Name="cancelButton" Margin="10">Cancel</Button>
                    </StackPanel>
                </Window>'
    
            $this.MainWindow = [Microsoft.UI.Xaml.Markup.XamlReader]::Load($xaml)
    
            $ClassScope = $this
            $WindowScope = $this.MainWindow
            
            $this.SharedConcurrentDictionary.App = $ClassScope # Terminal will crash on most properties and the object itself when printing to terminal.
            $this.SharedConcurrentDictionary.Window = $WindowScope
            $this.SharedConcurrentDictionary.Dispatcher = $WindowScope.DispatcherQueue
    
            $this.SharedConcurrentDictionary.OnloadFinished = $true
            # $this.MainWindow.Activate()
        }
        static [bool] Run($SharedConcurrentDictionary) {
            [Microsoft.Windows.ApplicationModel.DynamicDependency.Bootstrap]::Initialize(0x0010005)
    
            [Microsoft.UI.Xaml.Application]::Start({
                [PwshWinUIApp]::new($SharedConcurrentDictionary)
            })
            [Microsoft.Windows.ApplicationModel.DynamicDependency.Bootstrap]::Shutdown()
            return [PwshWinUIApp]::OkWasClicked
        }
    }
    
    [PwshWinUIApp]::Run($ConcurrentDict)
    '@
    
    # //Start app without window
    $AppSetupScriptBlock = [scriptblock]::Create($AppSetup)
    $null = $Powershell.AddScript($AppSetupScriptBlock)
    $Handle = $Powershell.BeginInvoke()
    
    # //Optional binding to class
    [NoRunspaceAffinity()]
    class binder {
        # //Should inherit IPropertyNotifyChanged
        # //or a dependency object
        binder(){}
        $tbContent = 'Without IPropertyNotifyChanged, this will not update'
    }
    $ConcurrentDict.binder = [binder]::new()
    
    # //Wait for app to finish loading
    while ($ConcurrentDict.OnloadFinished -ne $true) {
        Start-Sleep -Milliseconds 50
    }
    
    # //Send actions to dispatcher such as setting up buttons (Could also bind buttons through a class like above)
    $null = $ConcurrentDict.Dispatcher.TryEnqueue([scriptblock]::create({
        # //This is inside the Window thread/runspace
        # //Because ConcurrentDict is a shared variable, the Window thread can also access it
        # //We have less access compared to wpf, where you could traverse the wpf object on any thread.
        # //If you call $ConcurrentDict.Window.Content outside of this thread, it will be empty.
    
        $sp = $ConcurrentDict.Window.Content
        $ConcurrentDict.Window.Content.DataContext = $ConcurrentDict.binder
    
        $ok = $sp.FindName("okButton")
        $cancel = $sp.FindName("cancelButton")
        
        $ok.add_Click([scriptblock]::create({
            param($s, $e)
            Write-Verbose "sender is: $($s.Name)" -Verbose
            
            [PwshWinUIApp]::OkWasClicked = $true
            
            $ConcurrentDict.ThreadId = "Set from Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId)"
            $ConcurrentDict.Window.Close()
        }.ToString()))
        
        $cancel.add_Click([scriptblock]::create({
            param($s, $e)
            Write-Verbose "sender is: $($s.Name)" -Verbose
            
            $ConcurrentDict.ThreadId = "Set from Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId)"
            $ConcurrentDict.Window.Close()
        }.ToString()))
    }.ToString()))
    
    # //Finally show the window via dispatcher
    $Action = {$ConcurrentDict.Window.Activate()}.ToString()
    $NoContextAction = [scriptblock]::create($Action)
    $null = $ConcurrentDict.Window.DispatcherQueue.TryEnqueue($NoContextAction)
    
    "Current Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId)"
    $ConcurrentDict