Write PowerShell Output (as it happens) to WPF UI Control

10,003

Solution 1

Out of curiosity, have you considered that instead of PowerShell firing up WPF and displaying some output, it should be the other way around?

Perhaps you should be invoking PowerShell from within a WPF application, and capturing the output to be displayed?

http://www.codeproject.com/Articles/18229/How-to-run-PowerShell-scripts-from-C

Solution 2

I ran into the same question and seen quiet some suggestions that either refer to C# classes or examples where the UI is placed in the worker instead of the actual worker task. Anyway, after spending quite some time I figured it out:

Add-Type -AssemblyName PresentationFramework
$Xaml = New-Object System.Xml.XmlNodeReader([XML]@"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Test" WindowStartupLocation = "CenterScreen" ShowInTaskbar = "True">
    <Grid>
        <TextBox x:Name = "TextBox"/>
    </Grid>
</Window>
"@)

Function Start-Worker {
    $TextBox = $Window.FindName("TextBox")
    $SyncHash = [hashtable]::Synchronized(@{Window = $Window; TextBox = $TextBox})
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ThreadOptions = "ReuseThread"         
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)          
    $Worker = [PowerShell]::Create().AddScript({
        for($Progress=1; $Progress -le 10; $Progress++){
            $SyncHash.Window.Dispatcher.Invoke([action]{$SyncHash.TextBox.AppendText($Progress)})
            Start-Sleep 1                                                       # Some background work
        }
    })
    $Worker.Runspace = $Runspace
    $Worker.BeginInvoke()
}

$Window = [Windows.Markup.XamlReader]::Load($Xaml)                              # Requires -STA mode
$Window.Add_Loaded({Start-Worker})
[Void]$Window.ShowDialog()

For a windows control example see: PowerShell: Job Event Action with Form not executed

Share:
10,003
jmcdade
Author by

jmcdade

Updated on June 04, 2022

Comments

  • jmcdade
    jmcdade almost 2 years

    I've been reading blogs about writing to the UI from different runspaces (http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/).

    I'm basically trying to make it so I can click a button in the UI and run a PowerShell script and capture the output of that script as it happens and update the WPF UI control without freezing up the UI.

    I've tried a basic example of just writing some output directly, but it seems to hang the UI. I'm using runspaces and dispatcher, but I seem to be stuck on something.

    Any ideas?

    Thanks.

    Add-Type –assemblyName PresentationFramework
    Add-Type –assemblyName PresentationCore
    Add-Type –assemblyName WindowsBase
    
    
    $uiHash = [hashtable]::Synchronized(@{})
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"
    $newRunspace.Open() 
    $newRunspace.SessionStateProxy.SetVariable('uiHash',$uiHash)
    
    $psCmd = [PowerShell]::Create().AddScript({
    [xml]$xaml = @"
    <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            x:Name="Window" Title="Patcher" Height="350" Width="525" Topmost="True">
        <Grid>
            <Label Content="A Builds" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="88" RenderTransformOrigin="0.191,0.566"/>
            <ListBox HorizontalAlignment="Left" Height="269" Margin="10,41,0,0" VerticalAlignment="Top" Width="88"/>
            <Label Content="New Build" HorizontalAlignment="Left" Margin="387,10,0,0" VerticalAlignment="Top"/>
            <TextBox HorizontalAlignment="Left" Height="23" Margin="387,41,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
            <Label Content="B Builds" HorizontalAlignment="Left" Margin="117,10,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.528,-0.672"/>
            <ListBox HorizontalAlignment="Left" Height="269" Margin="103,41,0,0" VerticalAlignment="Top" Width="88"/>
            <Label Content="C Builds" HorizontalAlignment="Left" Margin="185,10,0,0" VerticalAlignment="Top"/>
            <ListBox HorizontalAlignment="Left" Height="269" Margin="196,41,0,0" VerticalAlignment="Top" Width="88"/>
            <Button x:Name="PatchButton" Content="Patch!" HorizontalAlignment="Left" Margin="426,268,0,0" VerticalAlignment="Top" Width="75"/>
            <RichTextBox x:Name="OutputTextBox" HorizontalAlignment="Left" Height="194" Margin="289,69,0,0" VerticalAlignment="Top" Width="218">
                <FlowDocument>
                    <Paragraph>
                        <Run Text=""/>
                    </Paragraph>
                </FlowDocument>
            </RichTextBox>
    
        </Grid>
    </Window>
    "@
    
    $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    $uiHash.Window = [Windows.Markup.XamlReader]::Load($reader)
    
    $uiHash.Button = $uiHash.Window.FindName("PatchButton")
    $uiHash.OutputTextBox = $uiHash.Window.FindName("OutputTextBox")
    
    $uiHash.OutputTextBox.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler]    {$uiHash.OutputTextBox.UpdateLayout()}, $null, $null)
    
    $uiHash.Window.ShowDialog() | Out-Null
    })
    $psCmd.Runspace = $newRunspace
    $data = $psCmd.BeginInvoke()
    
    # The next line fails (null-valued expression)
    <#
    $uiHash.OutputTextBox.Dispatcher.Invoke("Normal", [action]{
        for ($i = 0; $i -lt 10000; $++) {
            $uiHash.OutputTextBox.AppendText("hi")
        }
    })#>
    
  • jmcdade
    jmcdade about 10 years
    Yeah, I wanted to see if I could do it this way out of curiosity. I'm not really tied to one way or the other.
  • jmcdade
    jmcdade about 10 years
    Yeah, I found that out the hard way. When I try to use that block in the same script as the rest of it, I can't access the ui elements but it works via the console as the ui is running. Is that expected? In any case, is there a better way to do it performance wise, or should I flip the order as suggested by Doug?
  • boeprox
    boeprox about 10 years
    It really depends on your own personal preference. Some would say that you should write UIs in C# instead of PowerShell (much like Doug has suggested), but I prefer to write the in PowerShell and just make sure I am careful in my coding to watch out for any issues running commands against the UI. If your confident in your C# coding, then you can give it a shot, otherwise just keep moving forward with your current path.
  • jmcdade
    jmcdade about 10 years
    Could the UI thread handle the continuous pipeline output, or would it be pretty held up? I was in envisioning having script output in a text box so the user could keep an eye out, but still do other things with the UI. If the output from the script is just going to hog everything, I'll need a new approach.
  • jmcdade
    jmcdade about 10 years
    Trying to fit a WPF solution based on that approach was a giant headache. For my purposes I'm just going to host the PowerShell scripts within WinForms and be done with it.