WinForm Creating Controls in PowerShell Using Classes – Part 2

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 Form object.

# 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 $form in the old way is a VARIABLE that is ONLY available in the scope its created in, 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, $Obj = New-Object <SomeType> is almost an order of magnitude slower than $Obj = [<SomeType>]::new()

Even $Obj = [<SomeType>]@{} is still way faster though this one only works if there is a parameterless constructor, but lets us do fun things like $Obj = [<SomeType>=@{Property1="something";Property2=42} giving us a $Obj of type SomeType with Property1, and Property2 already set. It does comes at a performance cost the more properties we try to set so ::new() is pretty much the undisputed champ of object creation.

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 add_click(<Event Handler Method>) method of that control. Since PowerShell does not have access directly to the Events it uses the 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 [ScriptBlock] type into the add_<event> methods which is basically what a Function is. Button_Click is a [PSMethod] type.

The SECOND caveat is inside that ScriptBlock PowerShell automatically assigns $this to the 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 $this is NOT our form object in this scope.

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 (EventArgs 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 ScriptBlock PowerShell expects.

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 ObjectForm type inherit ALL that Form type has but also ALL that PSForm has. Meaning it ALSO comes with our GetHandler method built in.

From here you would probably separate PSForm type definition into its own file to dot source or even better into a module that a little using module 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.

Next time we’ll work on abstracting away the concept of creating and initializing Form Controls.

Leave a Reply

Your email address will not be published. Required fields are marked *