Writing your own PowerShell Hosting App (part 5)

In the last post, we got to the point that we were actually using the new host objects that we implemented, but we still hadn’t provided anything more than trivial implementations (throwing an exception) for the methods that make a custom host useful, e.g. the write-* functions.

Before we do that, we need to discuss interaction between PowerShell (the engine) and windows forms (though we would have had the same issue with WPF).  In PowerShell 1.0, the engine creates its own thread to run the Invoke() method, and doesn’t provide a way to change that thread’s apartment model, which is MTA.  The reason that is important is that to interact safely with Windows Forms (or WPF), you need to be in the same thread.  The bottom line is that when using the 1.0 object interface, you can’t directly interact with the window environment.  Which means that any hopes you had of writing some simple code to append text to the textbox in the WriteHost method are going to be dashed.  Unless, of course, you use the 2.0 object model.  The designers realized the shortcoming, and in 2.o they allow you to change the child thread to STA.

So now we have a couple of choices.  As I mentioned in part 3, I was purposely using the 1.0 object model, since 2.0 wasn’t final, and the 1.0 methods would work fine in a 2.0 install.   One thing we could easily do is switch the code to 2.0, set the threading model to STA, and go on our merry way.  Another approach would be to have the Host objects interact with the user interface indirectly.  One way to do that would be to simply have the host methods package their arguments into an object, and add the object into a queue that is consumed in a timer event handler on the form.  This works quite nicely, and provides an easy separation between the host and the interface.

For now, though, for the sake of simplicity (and to keep the code from getting longer than anyone would care to read), we’ll just use the 2.0 object model.  As I mentioned in part 4, I plan to create a project on Codeplex for a more complete host than I can really create in a tutorial.  It will include code to keep the host and interface separate (which I think I like better).

Here is the revised code in the form to use the 2.0 model (I’ve moved some of the declarations out of the Click method because the objects don’t need to be recreated each time):

Public Partial Class MainForm

public shared PowerShellOutput as textbox
private host as new PowerShellWorkBenchHost
private r As Runspace=RunspaceFactory.CreateRunspace(host)
	Public Sub New()
		' The Me.InitializeComponent call is required for Windows Forms designer support.
		Me.InitializeComponent()

		PowerShellOutput=txtOutput
       	r.ThreadOptions=PSThreadOptions.UseCurrentThread
        r.Open()
     End Sub

    Sub RunToolStripMenuItem1Click(sender As Object, e As EventArgs)

 		dim ps As powershell=PowerShell.Create()
        ps.Runspace=r
        ps.AddScript(txtScript.Text)
        ps.AddCommand("out-default")
        Dim output As Collection(Of psobject)
        output=ps.Invoke()

    End Sub

End Class

The line of code that will allow the host methods to interact with the form is:

r.ThreadOptions=PSThreadOptions.UseCurrentThread

A few other changes that should be noted are:

  • Adding a shared member PowerShellOutput to use in the host to update the textbox
  • Switching from out-string to out-default (now that we’re handling host output, we can let the default behavior send the objects in the pipeline to the host)
  • Removing the loop through the output (because of the previous point)

With that being said, I’ll make another comment.  If you’re trying to follow along with this series (as in, you have an editor open and are copying code in and trying it as you go), you’ll want to make sure you set a breakpoint on each of the throw statements in the host classes.  If you don’t do this, you won’t know what methods you need to implement (except by trial and error).  I have spent several hours debugging when the breakpoints would have showed me the problem immediately.  Please learn from my mistakes.

Now, we can finally get to coding the output routines.  We obviously need to implement some write* method, but there are several.   To figure out which one, I tried to run write-host “hello” and dir (fairly simple commands) and it turns out that we need to implement these methods for those to work:

  • WriteLine
  • Write  –both versions
  • PSHostRawUserInterface.ForegroundColor
  • PSHostRawUserInterface.BackgroundColor

I really wasn’t expecting the last color methods to come into play until we started passing them to the write-host cmdlet (which is why I lost so much time debugging).  Here are the implementations I’m using for now.  Note that we’re somewhat limited by the choice of a textbox (rather than a more fully-featured control) for output.
In PSHostUserInterface:

	Public Overloads Overrides Sub Write(value As String)
		If value=vblf Then
			mainform.PowerShellOutput.AppendText(vbcrlf)
		else
			MainForm.PowerShellOutput.AppendText(value	)
		End If
	End Sub

	Public Overloads Overrides Sub Write(foregroundColor As ConsoleColor, backgroundColor As ConsoleColor, value As String)
		MainForm.PowerShellOutput.AppendText(value	)
	End Sub

	Public Overloads Overrides Sub WriteLine(value As String)
		MainForm.PowerShellOutput.AppendText(value+vbcrlf)
	End Sub

and in PSHostRawUserInterface:

	Public Overloads Overrides Property ForegroundColor() As ConsoleColor
		Get
			return ConsoleColor.Black
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

	Public Overloads Overrides Property BackgroundColor() As ConsoleColor
		Get
			return ConsoleColor.White
		End Get
		Set
			Throw New NotImplementedException()
		End Set
	End Property

With those changes, we are (finally) using the custom host for output. Here’s an obligatory screenshot:

Output at last!

Output at last!

So what’s next? Obviously, we should fill in appropriate implementations for the other write-* functions. Other than write-progress, they shouldn’t prove any challenge. Write-progress, on the other hand, would really look nice as a progressbar (possibly in a status bar?). There are a few other things to consider:

  • clear-host is implemented as a function which uses the rawUI class to perform it’s stuff…that probably won’t work in out GUI app
  • Colors (if you choose to implement them) are going to be specified using the ConsoleColor class (which is different from the Color class used by Windows Forms)
  • Profiles….do you want to load them?  Do you want to have a profile specific to your new host?
  • What about interacting with the GUI in other ways?

The last point is the main thing that drove me to write my own host.  You may be fortunate enough to have a GUI tool to do all of your administration duties, but I suspect that most of us have several tools that we have to switch between to get stuff done.  And those tools are probably not powershell-ready.  Writing your own host allows you to build your “dream environment”, combining the best features of your favorite tools, and adding script-support in the process.

Next time, we’ll see about doing something different…adding data from powershell into a treeview control (in the host, of course).

As usual, please let me know if you’re enjoying this series.

Mike