Search code examples
c#.netpowershelltypes.net-assembly

How to load assembly before param block of a Powershell script


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


Solution

  • 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 loads the referenced assembly's type at runtime, and execution of a script 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 (note that the linked documentation is incorrect as of this writing; an issue has been raised).

      • As an aside: As of PowerShell 7.4.1, 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 GitHub issue #18482 and 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 #2074
    • Finally, a related problem exists in conjunction with using module statements that contain class and enum PowerShell definitions: Even though using module generally does make these available at parse time and therefore allows them to be referenced in other class definitions in the the body of the caller's script, they too cannot be used inside param(...) blocks - see GitHub issue #19676.