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/