1

In a .NET application hosting a PowerShell runspace, is it possible to export host application functions to that runspace so that I have, say, PowerShell cmdlets that cause effects on my UI?

My goal is simply to be able to report progress messages back to my host application UI from a long-running PowerShell script (since by hosting PowerShell I have no console window.) I'm imagining something like:

C#:

// Speculative pseudocode:

class ReportProgressCmdlet : Cmdlet { ... } // Reports a message on the UI.
...
myRunspace.Register(new ReportProgressCmdlet(myUI)); 

PowerShell:

Report-Progress "Starting step 1." # UI shows "Starting step 1."
...
Report-Progress "Step 1 failed."

Is this possible? If I'm going about it wrong, happy to hear different solutions (events?) I've read all of the following questions and many others like them, and I don't THINK they're what I'm looking for:

How to expose functionality to PowerShell from within your application

Powershell Call Assembly Delegate

How to programmatically add a PSCmdlet to a Powershell pipeline?

Provide a .NET method as a delegate callback

Community
  • 1
  • 1
user1454265
  • 868
  • 11
  • 25
  • 1
    If you're using a runspace, you can read from the runspace streams while the runspace is executing. That means that you can use e.g. Write-Progress in your runspace script, and then read the Progress stream from the runspace periodically and use that to display progress information in your UI. – mjolinor Mar 04 '15 at 21:58
  • After much thrashing about with my own answer below and then a PSHost implementation, this turned out to be a really useful technique for displaying nicely-formatted errors in combination with my PSHost (easier to work with an ErrorRecord object rather than the mess of lines you get by merging the error stream into your PSHost output.) For those curious, it's the PowerShell.Streams.Error.DataAdding event you want to hook to here, then grab the ItemAdded property from the event args. – user1454265 Mar 12 '15 at 20:14

2 Answers2

0

UPDATE: The technique I hit on below is good for arbitrary GUI actions, but for my purposes (displaying PowerShell output in a cleaned-up format) it turned out to be easier to implement a custom PSHost, PSHostUserInterface, and PSHostRawUserInterface so that the scripting side stays simpler (you work with the familiar Write-Output, Write-Host, Write-Error, et al. rather than some bespoke output API. This also keeps the script flexible in case you want to run it outside your host as well). See https://msdn.microsoft.com/en-us/library/ee706570%28v=vs.85%29.aspx for a good example.


Cmdlets turned out to be the wrong approach — the way to go is to expose GUI object instances to PowerShell via the "SessionState" APIs, then PowerShell code can use those directly. Here's how you do it:

Bring in the right namespaces:

using System.Management.Automation;
using System.Management.Automation.Runspaces;

Then create a Runspace and inject your GUI object instance as a PowerShell variable. (Here I'm passing a viewmodel that exposes a Messages collection, but you probably want to layer things better in the real world.) You can use either an InitialSessionState when creating the runspace, like:

var iss = InitialSessionState.CreateDefault();
iss.Variables.Add(new SessionStateVariableEntry("thing", this.DataContext, "it's the data context"));

using (Runspace runspace = RunspaceFactory.CreateRunspace(iss))
{
    runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;
    runspace.Open();

    using (PowerShell ps = PowerShell.Create())
    {
        ps.Runspace = runspace;
        ps.AddScript(@"$thing.Messages.Add('in which PowerShell can make stuff happen on the GUI via InitialSessionState')");
        ps.Invoke();
    }

    runspace.Close();
}

Or you can inject object instances after the Runspace is created and opened, using Runspace.SessionStateProxy.SetVariable(name, object). The code for that one's the same, except you don't have to inject an InitialSessionState.

Doug Finke gets the credit for this one, since I learned the technique from a screencast of his I found:

http://www.dougfinke.com/blog/index.php/2009/09/02/how-to-host-powershell-in-a-wpf-application/

Community
  • 1
  • 1
user1454265
  • 868
  • 11
  • 25
0

I made a progress bar in WPF + XAML, and running it in a different Runspace. This code might be exactly what you need.

https://gallery.technet.microsoft.com/Script-New-ProgressBar-329abbfd

This is an example on how to use the function i made to run a loop, update the new Runspace. And also detect if the window/progressbar was closed, and stop executing your code.

# Create the window form in a different namespace 
Load-Window 

For ($i = 0; $i -lt 100; $i++) { 

  #Update the progressbar 
  $Result = Update-ProgressBar -Percent $i -Label1Text "$i / 100" 

  # If you want to simply check if the window is still active
  # Update-Window will return if the form/window is active 
  # $Result = Update-Window 

  #If the window is close, stop the loop 
  If ($Result -eq $False) { Break } 

  Sleep -Milliseconds 100 

} 

Close-Window 
Marc Kellerman
  • 466
  • 3
  • 10