Powershell Functions using ToolStripMenuItem work in an unintended way - winforms

Something that I can not understand while creating a GUI with PowerShell
Reappearance:
Add-Type -AssemblyName System.Windows.Forms
function Build-MainForm2 () {
$form = New-Object Windows.Forms.Form
# MenuStrip
$menuStrip = New-Object Windows.Forms.MenuStrip
$menuStrip.Dock = 'Top'
# MenuStripItem
$msFile = New-Object -TypeName Windows.Forms.ToolStripMenuItem -ArgumentList 'File'
# Build Form
$menuStrip.Items.Add($msFile)
$form.Controls.Add($menuStrip)
return $form
}
$mainForm = Build-MainForm2
$mainForm.showDialog()
When I run this script PowerShell ISE show me error like this
[System.Object[]]에 이름이 'showDialog'인 메서드가 없으므로 메서드를 호출하지 못했습니다.
위치 D:\ProgramData\stgr\TWT\win\TwT\src\wtf.ps1:21 문자:25
+ $mainForm.showDialog <<<< ()
+ CategoryInfo : InvalidOperation: (showDialog:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
What it means is that there is no method
But when I comment out the 15th line
$menuStrip.Items.Add($msFile) -> #$menuStrip.Items.Add($msFile)
The script works fine
Go back to the beginning. I was wondering about the return value of Build-MainForm2, and when I tried to print out the value, it was an array with 0 and a Form instance.
(0, [System.Windows.Forms.Form])
After all, you can make the script work by changing the final code as follows:
$mainForm[1].showDialog()
But I still do not know why the return value was converted to this array.
I wonder if there is a neat solution to this.
Enviroment: PowerShell ISE 2.0 (Windows 7)

The Add method on Items returns an integer. You are not handling this, so it's getting inserted into your variable as an array item.
You can fix this by redirecting that output:
$null = $menuStrip.Items.Add($msFile)

Related

MessageBox dialog is not shown when a ps-script run through Command Prompt [duplicate]

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
}
}
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
}
'#
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.
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')
}
}

Get-ADUser triggered by Button

I have a button on a WinForm. After clicking on the button, a function will be called which should execute Get-ADUser cmdlet.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Data
Import-Module ActiveDirectory
$ctl_frm_aduserlist = New-Object System.Windows.Forms.Form -Property #{
Size = New-Object System.Drawing.Size(500,500)
StartPosition = "CenterScreen" }
$ctl_btn_generatepreview = New-Object System.Windows.Forms.Button -Property #{
Size = New-Object System.Drawing.Size(200,30)
Location = New-Object System.Drawing.Point(10,30)
Text = "Generate Preview" }
$ctl_frm_aduserlist.Controls.Add($ctl_btn_generatepreview)
$ctl_btn_generatepreview.Add_Click({ GeneratePreview })
function GeneratePreview(){
Write-Host "GO"
Get-ADUser -Identity "user123" -Properties Name,SamAccountName | select Name,SamAccountName
Write-Host "END" }
$ctl_frm_aduserlist.ShowDialog()
Only the two "Write-Host" cmdlets will be executed by clicking on the button.
If I only execute the single line Get-ADUser in the ISE console, it works and I get the user object.
Why does Get-ADUser not work when triggered via button?
Thanks
First off, I don't have a full solution as I'm not an expert in WinForms, to feel free to correct/complete this answer.
As far as I know, the command is actually executed. You can check it by stepping though the code or Write-Host-ing bits of the object you collected with Get-ADUser and it will display the correct info:
function GeneratePreview() {
Get-ADUser -Identity "someone" | Write-Host
}
Note that if you replace Write-Host by Write-Output it does not work anymore.
The thing is, when you make a WinForms application, the standard output isn't the console anymore. I don't know where it is redirected to, but it's not visible by default. You need to either specify that you want to see your data in the console with write-host, export it to a file (Set-Content, Export-Csv, you name it) or display it in a WinForm element:
I added a new TextBox to your form (called $TextBox1 in my example), and changed GeneratePreview like this:
function GeneratePreview() {
$user = Get-ADUser "someone"
$TextBox1.Text = "$($user.name),$($user.samaccountname)"
}

Powershell SMO: This method does not support scripting data

I want to generate an import script for a MSSQL DB via Powershell (related to this question).
I tried doing this:
#Set-ExecutionPolicy RemoteSigned
$DB_NAME = "<<dbName>>"
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null
$srv = new-object "Microsoft.SqlServer.Management.SMO.Server" "<<server>>"
$conContext = $srv.ConnectionContext
$conContext.LoginSecure = $false
$conContext.Login = "<<user>>"
$conContext.Password = "<<password>>"
$srv = new-object Microsoft.SqlServer.Management.Smo.Server($conContext)
$srv.SetDefaultInitFields([Microsoft.SqlServer.Management.SMO.View], "IsSystemObject")
$db = $srv.databases[$DB_NAME]
$scripter = new-object "Microsoft.SqlServer.Management.Smo.Scripter" $srv
$scripter.Options.ScriptSchema = $false
$scripter.Options.ScriptData = $true
$scripter.Options.ScriptDrops = $false
$scripter.Script($db)
But executing this throws an error:
"This method does not support scripting data"
I also tried to set the output file option but this doesn't change anything.
Can you tell me what I did wrong?
Thanks!
Per the error, Scripter.Script does not support scripting data. This is documented. What isn't documented is what you're supposed to use instead, but it's EnumScript:
$scripter.EnumScript(#($db.Tables))
You must pass the tables, since simply scripting the database will yield nothing (as, technically, the database itself contains no data, its tables do).
(The #() forcibly converts the Tables collection to an array, since that's what EnumScript expects.)

Dynamically add and remove content in GUI form

I am trying to create in PowerShell a GUI window with dynamic content. I need to:
create a window with a random count of buttons (or other clickable items)
after a click button and related text label will be removed from the window
IMPORTANT: I cannot use a list or datagrid.
I have the following code but it still returns only last item value.
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(400,400)
$Array = New-Object System.Collections.ArrayList
$Array.add('AAA')
$Array.add('BBB')
$Array.add('CCC')
$Array.add('DDD')
foreach ($item in $Array) {
New-Variable -Force -Name "button$membershipCount" -Value (New-Object System.Windows.Forms.Button)
$thisButton = Get-Variable -ValueOnly -Include "button$membershipCount"
$thisButton.Location = New-Object System.Drawing.Size(175,(35+26*$membershipCount))
$thisButton.Size = New-Object System.Drawing.Size(250,23)
$thisButton.Text = $item
$thisButton.Add_Click({(Write-Host $thisButton.Text | Out-Null)})
$form.Controls.Add($CHANGEButton)
}
I also tried Invoke-Expression, but it doesn't return expected results:
Invoke-Expression -Command "`$thisButton.Add_Click({`$x=`"$($item)`";`write-host $x})"
Or any better idea how can I get details which button was clicked since the number of buttons is random?
The problem is that the scriptblock inside add_Click() now contains a reference to $thisbutton, which at runtime will have been replaced with the last value in the foreach loop - this is expected behavior.
You can do 1 of 2 things here.
1. Capture the $thisButton.Text (or $item) value in a closure:
# Piping to Out-Null has zero effect here, just remove it
$thisButton.Add_Click({Write-Host $thisButton.Text}.GetNewClosure())
2. Use the event arguments to determine which button was clicked at runtime:
$thisButton.Add_Click({param($Sender,$EventArgs) Write-Host $Sender.Text})

Powershell pack uri object

I'm trying to create a pack ui referencing a xaml resource inside of an assembly file in powershell. After reading this post I tried to do this:
$resource = new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")
The I get an error noting that it is expecting a port since there are two colons.
Can anyone please advice?
You can go about this one of two ways. One is to load up and init the WPF infrastructure:
Add-Type -AssemblyName PresentationFramework,PresentationCore
[windows.application]::current > $null # Inits the pack protocol
new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")
The other way is to manually register the pack protocol:
$opt = [GenericUriParserOptions]::GenericAuthority
$parser = new-object system.GenericUriParser $opt
if (![UriParser]::IsKnownScheme("pack")) {
[UriParser]::Register($parser,"pack",-1)
}
new-object system.uri("pack://application:,,,/WPFResource;component/test.xaml")

Resources