Reputation: 141
Below is the exact code that I am having trouble with.
A brief description:
I am trying to set up a PowerShell class that will hold objects of different types for easy access. I've done this numerous times in C#, so I thought it would be fairly straight forward. The types wanted are [System.Printing] and WMI-Objects.
Originally I had tried to write the class directly to my PowerShell profile for easy usage, but my profile fails to load when I have to class code in it. Saying that it can’t find the type name "System.Printing.PrintServer", or any other explicitly listed types.
After that failed, I moved it to its own specific module and then set my profile to import the module on open. However, even when stored in its own module, if I explicitly list a .NET type for any of the properties, the entire module fails to load. Regardless of whether I have added or imported the type / dll.
The specific problem area is this:
[string]$Name
[System.Printing.PrintServer]$Server
[System.Printing.PrintQueue]$Queue
[System.Printing.PrintTicket]$Ticket
[System.Management.ManagementObject]$Unit
[bool]$IsDefault
When I have it set to this, everything "kind of" works, but then all my properties have the _Object type, which is not helpful.
[string]$Name
$Server
$Queue
$Ticket
$Unit
$IsDefault
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
Class PrinterObject
{
[string]$Name
[System.Printing.PrintServer]$Server
[System.Printing.PrintQueue]$Queue
[System.Printing.PrintTicket]$Ticket
[System.Management.ManagementObject]$Unit
[bool]$IsDefault
PrinterObject([string]$Name)
{
#Add-Type -AssemblyName System.Printing
#Add-Type -AssemblyName ReachFramework
$this.Server = New-Object System.Printing.PrintServer -ArgumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
}
PrinterObject([string]$Name, [bool]$IsNetwork)
{
#Add-Type -AssemblyName System.Printing
#Add-Type -AssemblyName ReachFramework
if($IsNetwork -eq $true) {
$this.Server = New-Object System.Printing.PrintServer ("\\Server")
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`""
}
else {
$This.Server = New-Object System.Printing.PrintServer -argumentList [System.Printing.PrintSystemDesiredAccess]::AdministrateServer
$this.Queue = New-Object System.Printing.PrintQueue (($this.Server), ($this.Server.GetPrintQueues() |
Where-Object {$_.Name -match $Name} | Select-Object -ExpandProperty Name))
$this.Ticket = $this.Queue.UserPrintTicket
$this.Unit = Get-WmiObject -Query "SELECT * FROM Win32_Printer WHERE Name LIKE `"%$Name%`"" }
}
[void]SetPrintTicket([int]$Copies, [string]$Collation, [string]$Duplex)
{
$this.Ticket.CopyCount = $Copies
$this.Ticket.Collation = $Collation
$this.Ticket.Duplexing = $Duplex
$this.Queue.Commit()
}
[Object]GetJobs($Option)
{
if($Option -eq 1) { return $this.Queue.GetPrintJobInfoCollection() | Sort-Object -Property JobIdentifier | Select-Object -First 1}
else { return $this.Queue.GetPrintJobInfoCollection() }
}
static [Object]ShowAllPrinters()
{
Return Get-WmiObject -Class Win32_Printer | Select-Object -Property Name, SystemName
}
}
Upvotes: 6
Views: 6307
Reputation: 437062
To complement PetSerAl's helpful answer, which explains the underlying problem and contains effective solutions, with additional background information:
To recap:
As of PowerShell 7.3.1, a PowerShell class
definition can only reference .NET types that have already been loaded into the session before the script is invoked.
Because class
definitions are processed at parse time of a script, rather than at runtime, Add-Type
-AssemblyName
calls inside a script execute too late for the referenced assemblies' types to be known to any class
definitions inside the same script.
A using assembly
statement should solve this problem, but currently doesn't:
using assembly
should be the parse-time equivalent of an Add-Type
(analogous to the relationship between using module
and Import-Module
), but this hasn't been implemented yet, because it requires extra work to avoid the potential for undesired execution of arbitrary code when an assembly is loaded.
Implementing a solution has been green-lighted in GitHub issue #3641, and the necessary work is being tracked as part of GitHub issue #6652 - but it is unclear when this will happen, given that the issue hasn't received attention in several years.
Upvotes: 2
Reputation: 22102
Every PowerShell script is completely parsed before the first statement in the script is executed. An unresolvable type name token inside a class definition is considered a parse error. To solve your problem, you have to load your types before the class definition is parsed, so the class definition has to be in a separate file. For example:
Main.ps1:
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
. $PSScriptRoot\Class.ps1
Class.ps1:
using namespace System.Management
using namespace System.Printing
Class PrinterObject
{
[string]$Name
[PrintServer]$Server
[PrintQueue]$Queue
[PrintTicket]$Ticket
[ManagementObject]$Unit
[bool]$IsDefault
}
The other possibility would be embed Class.ps1
as a string and use Invoke-Expression
to execute it. This will delay parsing of class definition to time where types is available.
Add-Type -AssemblyName System.Printing
Add-Type -AssemblyName ReachFramework
Invoke-Expression @'
using namespace System.Management
using namespace System.Printing
Class PrinterObject
{
[string]$Name
[PrintServer]$Server
[PrintQueue]$Queue
[PrintTicket]$Ticket
[ManagementObject]$Unit
[bool]$IsDefault
}
'@
Upvotes: 12
Reputation: 2835
A better solution (than just invoking the entire class in a string) would be to just create your objects and pass them to the class as parameters. For example, this runs fine:
Add-Type -AssemblyName PresentationCore,PresentationFramework
class ExampleClass {
$object
ExampleClass ($anotherClass) {
$this.object = $anotherClass
}
[void] Show () {
$this.object::Show('Hello')
}
}
$y = [ExampleClass]::new([System.Windows.MessageBox])
$y.Show()
However, if you were to do something like this, you can expect Unable to find type [System.Windows.MessageBox].
Add-Type -AssemblyName PresentationCore,PresentationFramework
class ExampleClass2 {
$object
ExampleClass () {
$this.object = [System.Windows.MessageBox]
}
[void] Show () {
$this.object::Show('Hello')
}
}
Upvotes: 0