0

I am trying to implement background commands that would initiate after the user clicks a button, and so far I have always ended up with my UI locking up while the job is on-going. I am currently using a somewhat time consuming for loop to check if my UI locks up. What can I do to let the job work in the background as the UI is freed up. I am not sure I want to complicate the code by adding runspaces. What am I doing incorrectly?

$inputXML = @"
<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication2"
        mc:Ignorable="d"
        Title="Create User" Height="448.05" Width="656.017" ResizeMode="NoResize">
    <Grid Margin="0,0,-6.8,-0.8" Background="#FFD7D7D7">
        <Grid.RowDefinitions>
            <RowDefinition Height="403*"/>
            <RowDefinition Height="18*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button x:Name="Create_User" Content="Create User" HorizontalAlignment="Left" Margin="67,224,0,0" VerticalAlignment="Top" Width="300" Height="26" RenderTransformOrigin="0.551,-0.671" IsEnabled="True">
            <Button.Background>
                <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                    <GradientStop Color="#FFF3F3F3" Offset="0"/>
                    <GradientStop Color="#FFEBEBEB" Offset="0.5"/>
                    <GradientStop Color="#FFDDDDDD" Offset="0.5"/>
                    <GradientStop Color="#FFCDCDCD" Offset="1"/>
                </LinearGradientBrush>
            </Button.Background>
        </Button>
        <Label x:Name="fname" Content="" HorizontalAlignment="Left" Margin="43,11,0,0" VerticalAlignment="Top" Height="26" Width="10"/>
        <Label x:Name="fname1" Content="First Name" HorizontalAlignment="Left" Margin="88,54,0,0" VerticalAlignment="Top" Width="72" RenderTransformOrigin="0.513,1.469" Height="26"/>
        <Label x:Name="lname" Content="Last Name" HorizontalAlignment="Left" Margin="88,83,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.167,-0.458" Width="72" Height="25"/>
        <TextBox x:Name="fnametxt" Height="23" Margin="167,54,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="0.501,0.452" HorizontalAlignment="Left" Width="136"/>
        <Button x:Name="exitbtn" Content="Exit" HorizontalAlignment="Left" VerticalAlignment="Top" Width="135" Margin="447,365,0,0" Height="38" RenderTransformOrigin="0.489,0.462"/>
        <Label x:Name="label" Content="Password" HorizontalAlignment="Left" Margin="92,113,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.064,0.601" Height="26" Width="68"/>
        <PasswordBox x:Name="passwordBox" Margin="167,113,0,0" VerticalAlignment="Top" Height="24" HorizontalAlignment="Left" Width="136"/>
        <TextBox x:Name="stsbox" HorizontalAlignment="Left" Height="62" Margin="67,267,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="300" Background="#FFDADADA" Foreground="Black" Opacity="0.45" SelectionBrush="#FF1D6EBF" RenderTransformOrigin="0.503,-0.59" IsReadOnly="True"/>
        <TextBox x:Name="lnametxt" Height="23" Margin="167,85,0,0" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Left" Width="136"/>
    </Grid>
</Window>
"@ 

$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[xml]$xaml = $inputXML

#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml) 
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}
$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global}


Function global:Get-FormVariables{
if ($global:ReadmeDisplay -ne $true) {$global:ReadmeDisplay=$true}
#write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
} 


Get-FormVariables


 $WPFCreate_User.Add_Click({

$test = { for($i=1; $i -le 100000; $i++){
$z = $i + $z
$z
}}

$job = Start-Job $test 
Wait-Job $job

Receive-Job $job -OutVariable results
Remove-Job $job
$WPFstsbox.text = "$results`n"
                           }) 

 $WPFexitbtn.add_click({
 $form.Close() | out-null
 exit})

$form.ShowDialog() | Out-null
briantist
  • 45,546
  • 6
  • 82
  • 127
Bregs
  • 87
  • 3
  • 15

1 Answers1

1

Yes, PSJobs are truly "background" jobs.

When you call Start-Job, a separate process is started, executing your job/command.

In your script, the job itself doesn't block the calling thread, but your subsequent command (Wait-Job $job) does.


If you simply fire off Start-Job and return from the Click handler, your UI wouldn't lock up:

$button.add_Click({
  $jobCode = { Start-Sleep -Seconds 10 }
  $job = Start-Job $jobCode
})

you'll see that the UI becomes responsive again in way lees than 10 seconds.

The problem is that you no longer have access to the $job and no longer control over when it is to be displayed.

To compensate for this, you need background thread or a timed event that can periodically check up on the job(s) for you and output the result.

You could use a Timer and PowerShell's builtin eventing infrastructure for this:

# Create timer
$backgroundTimer = New-Object System.Timers.Timer
# Set interval (ms)
$backgroundTimer.Interval = 500
# Make sure timer stops raising events after each interval
$backgroundTimer.AutoReset = $false

# Have powershell "listen" for the event in the background
Register-ObjectEvent $backgroundTimer -EventName 'Elapsed' -SourceIdentifier 'timerElapsed' -Action {

  # Loop through any completed jobs with data to return, from "oldest" to "newest"
  while(($finishedJob = Get-Job |Where-Object {$_.State -eq 'Completed' -and $_.HasMoreData}|Select-Object -First 1))
  {
    # Update the text box on your WPF form with the results
    # NOTE: the event is executed in a separate scope, thus the "global:" scope prefix
    $global:WPFstsbox.Text = "$(Receive-Job $finishedJob)`n"

    # Clean up the job
    Remove-Job $finishedJob
  }

  # Restart timer
  $backgroundTimer.Start()
}

# Enable the timer
$backgroundTimer.Enabled = $true
Community
  • 1
  • 1
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • Thanks for point that out, I was literally smacking my head trying to figure this out. In the case of using background threads, I tried using your method and it seems to spit a `Cannot Subscribe to specified event. A subscriber with the source identifier 'timerElapsed' already exists.` error. – Bregs Jan 06 '16 at 13:16
  • Forgive for the second comment, couldnt edit my earlier one. It seems that after I run your code, the SourceIdentifier still persists in the background and therefore when I go to re-run the code, powershell spits out the error saying the source identifier already exists. I looked into using Unregister-Event after the job has finished, however I am not having any luck with the cmdlet. As for right now, I am having to assign a new SourceIdentifier attribute every time I go to re-run the code and its working as I would like! I will try and figure out how to get rid of the error I mentioned earlie – Bregs Jan 06 '16 at 13:37
  • @Bregs correct, you need to manually clean up the event registrations when you close your WPF app. Sorry, I should have mentioned that. Use `Unregister-Event` for this – Mathias R. Jessen Jan 06 '16 at 14:10
  • I am running into a weird problem where when I am working in ISE, I can get the field to populate upon clicking the button, however running the script from the powershell console requires me to click the button twice before the results get populated. Is there a way around this? all I have done is added the `Unregister-Event` cmdlet. – Bregs Jan 06 '16 at 16:10
  • Where exactly have you added `Unregister-Event`? The code in my answer goes BEFORE and `Unregister-Event` AFTER `$form.ShowDialog()`. – Mathias R. Jessen Jan 06 '16 at 16:52
  • adding `Unregister-Event` after `$form.ShowDialog()` creates the same issue in ISE (Clicking twice to update the statusbox). I ended up adding the `Unregister-Event` right before `Remove-Job $finishedJob` in your code. It seems odd to me to clean up registrations in the event action itself, but its the only way I have gotten the statusbox to populate upon clicking the button. – Bregs Jan 06 '16 at 17:37