Gordon
Gordon

Reputation: 6863

.NET types in PowerShell classes

I am trying to get my head around PowerShell Class best practices, and running into some confusion with a simple class to handle Balloon Tips.

Add-Type -assemblyName:System.Drawing
Add-Type -assemblyName:System.Windows.Forms

class PxMessage {
    static [PxMessage] $instance
    static $balloon
    static $defaultIcon

    static [PxMessage] GetInstance($processIcon) {
        if ([PxMessage]::instance -eq $null) {
            [PxMessage]::instance = [PxMessage]::new()
            #[PxMessage]::balloon = [Windows.Forms.NotifyIcon]::new()
            [PxMessage]::balloon = New-Object Windows.Forms.NotifyIcon
            [PxMessage]::defaultIcon = $processIcon
        }

        return [PxMessage]::instance
    }

    [Void] SendMessage ([String]$title, [String]$message, [String]$messageIcon) {
        [PxMessage]::balloon.icon = [PxMessage]::defaultIcon
        [PxMessage]::balloon.balloonTipTitle = $title
        [PxMessage]::balloon.balloonTipText = $message
        [PxMessage]::balloon.balloonTipIcon = $messageIcon
        [PxMessage]::balloon.visible = $true 
        [PxMessage]::balloon.ShowBalloonTip(0)
        [PxMessage]::balloon.Dispose
    }
}


$processIcon = [System.Drawing.Icon]::ExtractAssociatedIcon($(Get-Process -id:$PID | Select-Object -expandProperty:path))
$message = [PxMessage]::GetInstance($processIcon)

$message.SendMessage('Title', "$(Get-Date)", 'Info')

I have two questions:

1: Why does [PxMessage]::balloon = New-Object Windows.Forms.NotifyIcon work, but [PxMessage]::balloon = [Windows.Forms.NotifyIcon]::new() does not (Unable to find type error)? And does this suggest that using [Type]::new() is not yet fully supported, and for consistency sake I am better off using New-Object everywhere? Or at least everywhere in my own Classes?

2: I would like to type my properties & parameters, but I also get Unable to find type errors when I type the $balloon & $defaultIcon properties, or if I type the $processIcon parameter in the GetInstance method.

Obviously I can type Properties, even with my type being defined. So what is different about the two [System.Drawing] & [System.Windows.Forms], and is this a bug, or a feature? And are there other types that behave similarly?

Upvotes: 4

Views: 698

Answers (1)

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174435

This is essentially a race condition!

When PowerShell starts executing a script file, it goes through 3 phases:

  • Parsing
  • Compilation
  • Execution

The very first thing that is processed in the compilation phase, (so before execution even begins) is:

  • All using statements at top of the script
  • Any type definitions - any class or enum keywords - are compiled separately

So the errors related to resolving the [Windows.Forms.NotifyIcon] type literal inside the class definition are actually thrown before Add-Type -AssemblyName:System.Windows.Forms gets a chance to ever run!

Couple of options:

Nested scripts

Write a separate loader script that loads the dependency:

# loader.ps1
Add-Type -AssemblyName System.Windows.Forms,System.Drawing
. .\scriptWithTypes.ps1
# scriptWithTypes.ps1
class ClassDependentOnForms
{
  [Windows.Forms.NotifyIcon]$BalloonTipIcon
}

With modules

With modules it's a bit simpler to manage dependencies ahead of compiling the custom type definitions - just add the assembly names as RequiredAssemblies to the module manifest:

New-ModuleManifest ... -RootModule moduleWithTypes.psm1 -RequiredAssemblies System.Windows.Forms,System.Drawing

Load from disk with using assembly ...

If the required assembly's path is known, you can load it at parse time with a using assembly statement:

using assembly '.\path\to\System.Drawing.dll'
using assembly '.\path\to\System.Windows.Forms.dll'

class FormsDependentClass
{
  [Windows.Forms.NotifyIcon]$BallonTipIcon
}

For your use case, this last one is not very attractive because you'd need to hardcode the assembly instead of just loading it from the GAC by name.


Why does this occur in the first place?

This behavior might be slightly confusing because everything else in PowerShell is just straightforward "one statement at a time"-interpretation.

The reason for this exception is to allow scripts and functions to encapsulate custom parameter types:

param(
  [CustomEnumType]$Option
)

begin {
  enum CustomEnumType {
    None
    Option1
    Option2
  }
}

end {
  # do something based on $Option
}

Without this preemptive compilation of CustomEnumType at parse time, PowerShell would be unable to offer autocompletion and input validation for the -Option parameter argument - because it's type would not exist

Upvotes: 7

Related Questions