-1

I would like to launch a non-blocking UI from a parent Powershell script and receive UI messages like button clicks from the child job. I have this kind of messaging working using WinForms, but I prefer to use ShowUI because of how much less code it takes to create a basic UI. Unfortunately, though, I haven't found a way to send messages back to the parent job using ShowUI.

[Works] Forwarding Events When Using Start-Job

Using Start-Job, forwarding events from a child to a parent job is rather straightforward. Here is an example:

$pj = Start-Job -Name "PlainJob" -ScriptBlock {
    Register-EngineEvent -SourceIdentifier PlainJobEvent -Forward
    New-Event -SourceIdentifier PlainJobEvent -MessageData 'My Message'
}

Wait-Event | select SourceIdentifier, MessageData | Format-List

As expected, it prints out:

SourceIdentifier : PlainJobEvent
MessageData      : My Message

[Does Not Work] Forwarding Events When Using Start-WPFJob

Using Start-WPFJob, on the other hand, does not seem to forward events from the child to the parent. Consider this example:

Import-Module ShowUI
$wj = Start-WPFJob -ScriptBlock {
    Register-EngineEvent -SourceIdentifier MySource -Forward

    New-Button "Button" -On_Click { 
        New-Event -SourceIdentifier MySource -MessageData 'MyMessage'
        }
}

Wait-Event | select SourceIdentifier, MessageData | Format-List

Running this example produces this window:

enter image description here

Clicking on the button, however, does not yield an event in the parent job.


  1. Why doesn't the Start-WPFJob example yield events to the parent job?
  2. Is there some other way to use ShowUI to produce a button in a non-blocking manner and receive events from it?
alx9r
  • 3,675
  • 4
  • 26
  • 55
  • Oooh, that's a great question. Or idea. I mean, it would be nice if the WPFJob forwarded events, I wonder what's required... – Jaykul Dec 23 '14 at 22:15
  • Forwarding events from WPFJob would open up a whole world of use cases for ShowUI. MVC applications would become possible in very few lines of code. – alx9r Dec 24 '14 at 20:45
  • Yeah, I'm playing with it. I'm not sure why you want to do that though. I mean, why do you want events from the UI to go to a different process? You can stream output ... – Jaykul Dec 25 '14 at 06:12
  • I'm not sure what you mean by streaming output. I've got a state machine that manages long-running jobs. The state machine is triggered by a variety of powershell events originating from the .NET framework. The state machine needs its own thread because it blocks on Wait-Event. The UI needs its own thread because it blocks somewhere in WPF. In order for a user to, for example, pause/resume the state machine, events need to get sent from the UI to the state machine runspace...somehow. – alx9r Dec 25 '14 at 07:01
  • I can't get this working, and I'm going to have to get some help, but not until after the new year ;) I'll post an awful workaround below... – Jaykul Dec 30 '14 at 07:12

1 Answers1

1

I can't get engineevents to forward properly so far (actually, I can't even get them to do anything, as far as I can tell), I think your best bet is to run the WPFJob, and instead of New-Event, update the $Window UIValue, and then from your main runspace, instead of Wait-Event, use Update-WPFJob in a loop.

I would stick this function into the module (actually, I will add it for the 1.5 release that's in source control but not released yet):

function Add-UIValue {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [Windows.FrameworkElement]    
        $Ui,

        [PSObject]
        $Value
    )
    process {
        if ($psBoundParameters.ContainsKey('Value')) {
            Set-UIValue $UI (@(Get-UIValue $UI -IgnoreChildControls) + @($Value))
        } else {
            Set-UIValue -Ui $ui 
        }        
    }   
}

And then, something like this:

$job = Start-WPFJob {
  Window {
    Grid -Rows "1*", "Auto" {
      New-ListBox -Row 0 -Name LB -Items (Get-ChildItem ~ -dir)
      Button "Send" -Row 1 -On_Click { Add-UIValue $Window $LB.SelectedItem }
    }
  } -SizeToContent "Width" -MinHeight 800
}

Every time you click, would add the selected item to the UI output (if you run that window without the job and click the button a couple of times, then close the window, you'll get two outputs).

Then you can do something like this in the host instead of Wait-Event:

do {
  Update-WPFJob -Job $job -Command { Get-UIValue $Window -IgnoreChildControls } -OutVariable Output
  Start-Sleep -Mil 300
} while (!$Output)
Jaykul
  • 15,370
  • 8
  • 61
  • 70
  • OK, I think I finally grok what you're doing. That _is_ doozy of a workaround, but I think I can encapsulate most of the ugliness away. Can you clarify this one point for me: After running `$job = Start-WPFJob ...` should I expect results from that `do { ... } while ...` loop event without closing the window? – alx9r Jan 09 '15 at 23:34
  • 1
    The Update-WPFJob command will output the results of Get-UIValue (and store them in $Output, which will stop the loop). You would be able to then do something with the $Output... – Jaykul Jan 10 '15 at 20:58