Convert Forms GUI code to WPF, threading diffrences

Hi,

I’m trying to convert relative simple WF code but it has neet features that I consider crucial for my app:

The code below:

  • has a button that launch commandline process/tool’
  • it’s able to display process output in real time inside textbox
  • the most important part of the code is $process.SynchronizingObject = $form which prevents crashing of pwsh process, this cannot be used for WPF
[reflection.assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[reflection.assembly]::LoadWithPartialName("System.Drawing") | Out-Null

[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object 'System.Windows.Forms.Form'
$buttonRunProcess = New-Object 'System.Windows.Forms.Button'
$richtextboxOutput = New-Object 'System.Windows.Forms.RichTextBox'
$buttonExit = New-Object 'System.Windows.Forms.Button'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'

$buttonExit_Click={
    $form.Close()
}

$buttonRunProcess_Click = {
    $buttonRunProcess.Enabled = $false
    $richtextboxOutput.Clear()

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo.FileName = 'ping.exe'
    $process.StartInfo.Arguments = 'google.com'
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.CreateNoWindow = $true
    $process.StartInfo.RedirectStandardInput = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.EnableRaisingEvents = $true
    $process.SynchronizingObject = $form
    $process.add_OutputDataReceived( {
        $richtextboxOutput.AppendText($_.Data)
        $richtextboxOutput.AppendText("`r`n") }
    )

    $process.Start() | Out-Null
    $process.BeginOutputReadLine()
}

$Form_Load={
    #Correct the initial state of the form to prevent the .Net maximized form issue
    $form.WindowState = $InitialFormWindowState
}

$Form_Closed={
    try
    {
        $buttonRunProcess.remove_Click($buttonRunProcess_Click)
        $buttonExit.remove_Click($buttonExit_Click)
        $form.remove_Form_Closed($processTracker_Form_Closed)
        $form.remove_Load($Form_Load)
        $form.remove_Form_Closed($Form_Closed)
    }
    catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
}

$form.SuspendLayout()

$form.Controls.Add($buttonRunProcess)
$form.Controls.Add($richtextboxOutput)
$form.Controls.Add($buttonExit)
$form.ClientSize = [System.Drawing.Size]::new(584, 362)
$form.Margin = '4, 4, 4, 4'
$form.MinimumSize = [System.Drawing.Size]::new(304, 315)
$form.Name = 'Redirect Process Output'
$form.StartPosition = 'CenterScreen'
$form.Text = 'Redirect Process Output'

$buttonRunProcess.Anchor = 'Bottom, Left'
$buttonRunProcess.Location = [System.Drawing.Point]::new(12, 327)
$buttonRunProcess.Name = 'buttonRunProcess'
$buttonRunProcess.Size = [System.Drawing.Size]::new(75, 23)
$buttonRunProcess.TabIndex = 0
$buttonRunProcess.Text = 'Run'
$buttonRunProcess.UseCompatibleTextRendering = $True
$buttonRunProcess.UseVisualStyleBackColor = $True
$buttonRunProcess.add_Click($buttonRunProcess_Click)

$richtextboxOutput.Anchor = 'Top, Bottom, Left, Right'
$richtextboxOutput.HideSelection = $False
$richtextboxOutput.Location = [System.Drawing.Point]::new(12, 12)
$richtextboxOutput.Name = 'richtextboxOutput'
$richtextboxOutput.ReadOnly = $True
$richtextboxOutput.Size = [System.Drawing.Size]::new(559, 305)
$richtextboxOutput.TabIndex = 6
$richtextboxOutput.Text = ''
$richtextboxOutput.WordWrap = $False

$buttonExit.Anchor = 'Bottom, Right'
$buttonExit.Location = [System.Drawing.Point]::new(497, 327)
$buttonExit.Name = 'buttonExit'
$buttonExit.Size =  [System.Drawing.Size]::new(75, 23)
$buttonExit.TabIndex = 2
$buttonExit.Text = 'E&xit'
$buttonExit.UseCompatibleTextRendering = $True
$buttonExit.UseVisualStyleBackColor = $True
$buttonExit.add_Click($buttonExit_Click)

$form.ResumeLayout()

$InitialFormWindowState = $form.WindowState
$form.add_Load($Form_Load)
$form.add_Closed($Form_Closed)
$form.ShowDialog()

The WPF code:


using namespace System.Linq.Expressions

Add-Type -AssemblyName PresentationCore, PresentationFramework

$Xaml = @"
<Window Name="RedirectProcessOutput" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Redirect Process Output" Height="330" Width="360">
    <Grid>
        <TextBox Name="kkx4gdl0o6fsh" Text="" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="Auto" Width="Auto" Margin="0,0,0,0" />
        <Button Name="kkx4gdkzqyhqj" Content="Run" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Bottom" Width="64"/>
        <Button Name="bExit" Content="Exit" HorizontalAlignment="Right" Margin="0,0,0,0" VerticalAlignment="Bottom" Width="64"/>
    </Grid>
</Window>
"@

$Window = [Windows.Markup.XamlReader]::Parse($Xaml)
[xml]$xml = $Xaml
$xml.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name $_.Name -Value $Window.FindName($_.Name) }

$kkx4gdkzqyhqj.Add_Click( {
    $process = New-Object System.Diagnostics.Process
    $process.StartInfo.FileName = 'ping.exe'
    $process.StartInfo.Arguments = 'localhost -n 3'
    $process.StartInfo.UseShellExecute = $false
    $process.StartInfo.CreateNoWindow = $true
    $process.StartInfo.RedirectStandardInput = $false
    $process.StartInfo.RedirectStandardOutput = $true
    $process.EnableRaisingEvents = $true
    #$process.SynchronizingObject = $Window # this works for Windows Forms, cannot be used for WPF
    
    $process.add_OutputDataReceived({# <- this is giving me crash
        $kkx4gdl0o6fsh.Text += $_.Data
    })
    $process.Start() | Out-Null
    $process.BeginOutputReadLine()
})

$Window.ShowDialog()

When I try to run WPF code, I’m getting:

An error has occurred that was not properly handled. Additional information is shown below. The PowerShell process will exit.
Unhandled exception. System.Management.Automation.PSInvalidOperationException: There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: 
        $kkx4gdl0…xt += $_.Data
    
   at System.Management.Automation.ScriptBlock.GetContextFromTLS()
   at System.Management.Automation.ScriptBlock.InvokeAsDelegateHelper(Object dollarUnder, Object dollarThis, Object[] args)
   at lambda_method1234(Closure , Object , DataReceivedEventArgs )
   at System.Diagnostics.Process.OutputReadNotifyUser(String data)
   at System.Diagnostics.AsyncStreamReader.FlushMessageQueue(Boolean rethrowInNewThread)
--- End of stack trace from previous location ---
   at System.Diagnostics.AsyncStreamReader.<>c.<FlushMessageQueue>b__18_0(Object edi)
   at System.Threading.QueueUserWorkItemCallback.Execute()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()

Can someone with WPF background help me understand how to fix this?

At first glance, it looks like you have not defined $_.Data as that is outside of your foreach loop?