Laying The Foundation:
Following up from previously. We begin by using, well
. using
using namespace Microsoft.PowerShell.Commands using namespace System.Windows.Forms using namespace System.Management.Automation using namespace System.Drawing using namespace System.Collections using namespace System.Collections.Generic
This takes advantage of PowerShell’s ability to pull in entire namespaces of types making most of the uses of Add-Type
obsolete. I also use the direct type format whenever possible
$something.EnumProperty = [SomeEnum]::SomeEnumValue
Over
$something.EnumProperty = "SomeEnumValue"
One because its clearer what’s going on, and two because intellisense. That being said my OCD hates seeing a bunch of fully qualified class types in my code so this lets me turn
$Form = [System.Windows.Forms.Form]::new()
into
$Form = [Form]::new()
MUCH better for my neurosis.
Next up we’ll taking advantage of Inheritance just like a C# WinForms project would. We don’t create a base
object. Form
# Old way Add-Type -Assembly System.Windows.Forms $form = New-Object Windows.Forms.Form
using namespace System.Windows.Forms Class ObjectForm : Form { Form() {} } $form - [ObjectForm]::new()
Yes its more lines of code but
in the old way is a VARIABLE that is ONLY available in the scope its created in, $form
ObjectForm
on the other hand is a TYPE that can be instantiated ANYWHERE we need it.
Also it is worth noting here that after 100,000 object creations each measured by Measure-Command
, in a quick and dirty set of tests including with property assignment,
is almost an order of magnitude slower than $Obj = New-Object <SomeType>
$Obj = [<SomeType>]::new()
Even
is still way faster though this one only works if there is a parameterless constructor, but lets us do fun things like $Obj = [<SomeType>]@{}
giving us a $Obj = [<SomeType>=@{Property1="something";Property2=42}
of type $Obj
with SomeType
, and Property1
already set. It does comes at a performance cost the more properties we try to set so Property2
is pretty much the undisputed champ of object creation.::new()
Attending Some Events
We’re going to take a trip to the future to explain this next part. PowerShell’s handling of WinForms Events is much more disjointed than in a pure C# WinForms.
In C# WinForms you assign an Event Handler directly to the event,
Button.Click += new System.EventHandler(this.Button_Click)
and your Event Handler method always has the same basic signature
private void Button_Click(object sender,EventArgs e){}
Which in the end is just syntax sugar to call the underlying
method of that control. Since PowerShell does not have access directly to the Events it uses the add_click(<Event Handler Method>)
add_
methods to accomplish the same thing.
So assuming we have
Class FormTest : Form { hidden [Button] $Button hidden [TextBox] $TextBox hidden [void] Button_Click([Object] $sender, [EventArgs] $e) { $this.TextBox.Text = "Clicked" }
Then we would call
$this.Button.add_click($this.Button_Click)
and promptly error out.
Turns out the FIRST caveat is PowerShell only lets you pass a
type into the [ScriptBlock]
methods which is basically what a add_<event>
is. Function
is a Button_Click
type.[PSMethod]
The SECOND caveat is inside that ScriptBlock PowerShell automatically assigns
to the $this
sender
object, and
to the $_
EventArgs
object. so even if we had
$this.Button.add_click({$this.TextBox.Text = "Clicked"})
It would again error out as
is NOT our form object in this scope.$this
We could work around this like this:
hidden [ScriptBlock] $ButtonClickHandler = { Button_Click($this,$_) } hidden [void] Button_Click([Object] $sender, [EventArgs] $e) { $this.TextBox.Text = "Clicked" }
Then we would call it like:
$this.Button.add_click($this.ButtonClickHandler)
Works great but we’d need one of those script blocks in between EVERY Event Handler method and that seems like a lot of work. Fortunately there is an easier way. Since the signatures of our ScriptBlock and Event Handler methods NEVER CHANGE (
types change but they are all still inherited from EventArgs
so to the type system nothing has changed) we just need a way to convert ANY Event Handler Method into the EventArgs
PowerShell expects.ScriptBlock
Something like:
hidden [scriptblock] GetHandler([PSMethod] $method) { return { Param([object] $sender, [EventArgs] $e) $method.Invoke($sender,$e) }.GetNewClosure() }
or in its nice one-liner
hidden [scriptblock] GetHandler([PSMethod] $method) { return { Param([object] $sender, [EventArgs] $e) $method.Invoke($sender,$e) }.GetNewClosure() }
Now we can call
$Button.add_click($this.GetHandler($this.Button_Click))
EVEN BETTER!!
But we’re still not done. GetHandler is something that even though its just one line would be redundant to include in every Form Class we create so instead we’re back to Inheritance.
Instead of what we have now
using namespace System.Windows.Forms Class ObjectForm : Form { hidden [scriptblock] GetHandler([PSMethod] $method) { return { Param([object] $sender, [EventArgs] $e) $method.Invoke($sender,$e) }.GetNewClosure() } [Button] $Button [TextBox] $TextBox ObjectForm() { $this.TextBox = [TextBox]::new() $this.Button = [Button]::new() $Button.add_click($this.GetHandler($this.Button_Click)) $this.Controls.Add($this.Button) $this.Controls.Add($this.TextBox) } hidden [void] Button_Click([Object] $sender, [EventArgs] $e) { $this.TextBox.Text = "Clicked" } }
We switch it up like this
using namespace System.Windows.Forms Class PSForm : Form { hidden [scriptblock] GetHandler([PSMethod] $method) { return { Param([object] $sender, [EventArgs] $e) $method.Invoke($sender,$e) }.GetNewClosure() } } Class ObjectForm : PSForm { [Button] $Button [TextBox] $TextBox ObjectForm() { $this.TextBox = [TextBox]::new() $this.Button = [Button]::new() $Button.add_click($this.GetHandler($this.Button_Click)) $this.Controls.Add($this.Button) $this.Controls.Add($this.TextBox) } hidden [void] Button_Click([Object] $sender, [EventArgs] $e) { $this.TextBox.Text = "Clicked" } }
So now we have TWO level of inheritance going on but that change means not ONLY does our
type inherit ALL that ObjectForm
type has but also ALL that Form
has. Meaning it ALSO comes with our PSForm
GetHandler
method built in.
From here you would probably separate
type definition into its own file to dot source or even better into a module that a little PSForm
pulls in for us for in ANY script we want to create an inherited Form Class from, and BOOM we’re ready to start rocking.using module PSForm
Next time we’ll work on abstracting away the concept of creating and initializing Form Controls.