1

i'm trying to write a powershell script file that has Parameters whose types require loading. A simple code example is below: [System.Windows.Forms.MessageBoxButtons] requires loading of system.windows.forms.

The problem is, the Param(...) block must be the very first in the script file. So:

  • i cannot place Add-Type as the first line in the file.
  • i tried with using assembly system.windows.forms but it errors out saying: Cannot load assembly 'System.Windows.Forms'. I think it could be possible by explicitly writing the dll file path, but it's ugly and not device-agnostic

So what can i do? Here's the code sample.

messagebox.ps1

# Add-Type -AssemblyName system.windows.forms # DOESN'T WORK, can't be placed before Param()
# using assembly System.Windows.Forms # DOESN'T WORK, can't find the assembly to load

Param(
    [string] $Text = '',
    [string] $Caption = '',
    [System.Windows.Forms.MessageBoxButtons] $Buttons = [System.Windows.Forms.MessageBoxButtons]::OK # REPORTS: Unable to find type [System.Windows.Forms.MessageBoxButtons].
)
    
[System.Windows.Forms.MessageBox]::Show($Text, $Caption, $Buttons)

Thanks


A similar question (about a user defined type, instead of a system type): Powershell script Param block validation requires a type defined in another script

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
aetonsi
  • 208
  • 2
  • 9
  • Please note: as i implied in my own first answer, i don't want to break parameters autocompletion – aetonsi Mar 17 '23 at 17:17
  • Maybe put `Add-Type` in a separate script and do `using module Otherscipt.ps1;` – Charlieface Mar 17 '23 at 17:28
  • @Charlieface that's a nice idea, unfortunately i just tried and powershell throws the `unable to find type` error **before** importing the module – aetonsi Mar 17 '23 at 17:40

2 Answers2

1

The best solution i've found by now is enclosing everything into an IISB (immediately invoked scriptblock) and splatting $args:

Add-Type -AssemblyName system.windows.forms

& {
param(
    [string] $Text = '',
    [string] $Caption = '',
    [System.Windows.Forms.MessageBoxButtons] $Buttons = [System.Windows.Forms.MessageBoxButtons]::OK
)

[System.Windows.Forms.MessageBox]::Show($Text, $Caption, $Buttons)

} @args

but this is ugly and it breaks autocompletion obviously, because the Param() now belongs to the scriptblock instead of the script

mklement0
  • 382,024
  • 64
  • 607
  • 775
aetonsi
  • 208
  • 2
  • 9
1

Unfortunately, there is no good solution as of PowerShell 7.3.3.

However, there is a - cumbersome - workaround that preserves parameter validation and tab-completion:

[CmdletBinding()]
Param(
    [string] $Text = '',
    [string] $Caption = '',
    # Implement tab-completion.
    [ArgumentCompleter({
      Add-Type -Assembly System.Windows.Forms
      [enum]::GetNames([System.Windows.Forms.MessageBoxButtons])
    })]
    # Given that we cannot strongly type the parameter,
    # make sure whatever value is passed is *convertible* to the desired type.
    [ValidateScript({ 
      Add-Type -Assembly System.Windows.Forms
      $null -ne [System.Windows.Forms.MessageBoxButtons] $_
     })]
    # Do NOT type-constrain the parameter (implies [object]).
    $Buttons = 'OK'
)

# Must still ensure the assembly is loaded here, because
# it hasn't bee loaded yet if neither tab-completion was used
# nor a -Buttons value was passed.
Add-Type -Assembly System.Windows.Forms

[System.Windows.Forms.MessageBox]::Show($Text, $Caption, [System.Windows.Forms.MessageBoxButtons] $Buttons)

Note how the Add-Type calls now happen inside parameter-attribute script blocks and the function body, which bypasses the syntax problem.


Background information:

  • The problem is that parameter declarations are parsed at script parse time, which comes before runtime, i.e. before actual execution of the script, and any .NET types directly referenced in such declarations must already be loaded into the session.

  • Not just Add-Type, but - perhaps surprisingly - also using assembly statements execute at runtime, and execution never happens if the parsing stage fails.

    • That is, even though a using assembly statement is syntactically allowed before a param(...) block (unlike Add-Type), it does not help here - its execution would come too late.

    • As an aside: As of PowerShell 7.3.3, using assembly - is actually fundamentally broken for well-known assemblies - see GitHub issue #11856.

  • The underlying problem also affects the use of types in class definitions - see this answer.

  • Fixing this limitation has been green-lit years ago, but no one has stepped up to implement it yet, as of the time of this writing - see GitHub issue #3641

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Urgh almost feels like PS is getting worse not better at this stuff, and it was bad to start with. – Charlieface Mar 18 '23 at 20:10
  • @Charlieface, I wouldn't say it's getting worse, but it's certainly taking its time at getting better - these issues have been known for years. – mklement0 Mar 18 '23 at 20:33
  • damn... that's scary. my actual final script would also have 3 more parameters from that assembly: having to do all of this for each parameter would be absurd :\ i thought that i could at least use `using assembly` with an absolute path but i didn't try it... – aetonsi Mar 19 '23 at 20:11
  • 1
    @aetonsi, yes, it is inconvenient, but currently the only solution if you want both tab completion and type safety. As noted in the answer, `using assembly` - whether with a known assembly name or a full path - does _not_ help. A fix has been green-lit many years ago, but no one has stepped to implement it yet - see https://github.com/PowerShell/PowerShell/issues/3641#issuecomment-297151112 – mklement0 Mar 20 '23 at 01:26
  • 1
    i'm going to use this for now.. thank you mr. Klement as always – aetonsi Mar 21 '23 at 10:17