Powershell GUI, Progress Bar, and Hash Tables - multithreading

Part of a script that I'm working on takes the specified username, searches active directory for computers named like the username, pings them, and then adds the online machines to an array for use later. I'd like to add a progress bar to the ping portion of the script but I'm running into problems. The form I have to present the progress bar freezes execution of the script if I invoke it prior to the ping loop.
Here is the code for the progress bar (pretty basic):
#Progress Bar
$objFormPBar = New-Object System.Windows.Forms.Form
$objFormPBar.Text = "Loading"
$objFormPBar.Size = New-Object System.Drawing.Size(200,100)
$objFormPBar.StartPosition = "CenterScreen"
$objFormPBar.Topmost = $True
$objFormPBar.Icon = $Icon
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Size = New-Object System.Drawing.Size(175,20)
$progressBar.Location = New-Object System.Drawing.Size(5, 20)
$progressBar.Minimum = 0
$progressBar.Maximum = 0
$objFormPBar.Controls.Add($progressBar)
Here is where I'm attempting to display it, this way halts execution of the script (called from inside a click handler function, thus the global variables):
$global:progressBar.Maximum = $computers.count
$global:objFormPBar.Add_Shown({$objFormPBar.Activate()})
[void] $global:objFormPBar.ShowDialog()
Foreach ($computer in $computers) {
$computer = $computer.trim()
If(Test-Connection $computer -Count 1) {
$arrayComputers.add($computer) | Out-Null
}
$global:progressBar.Increment(1)
}
$global:objFormPBar.Close()
Looking into the problem I've discovered that you can run the progress bar in a separate thread and pass variables between the two (See: Marquee Progress Bar freezes in Powershell ). However I have had no luck getting it to work or even to get another thread to launch. How can I get this progress bar and the form along with it to run in another thread?

Why don't you just use the built-in UI for reporting progress in PowerShell? It's a one line call to Write-Progress e.g.:
$computers = 1..10 | % {"server$_"}
$numComputers = $computers.Length
for ($ndx = 0; $ndx -le $numComputers; $ndx++) {
$computer = $computers[$ndx].Trim()
Write-Progress "Testing connection to $computer" -PercentComplete ($ndx/$numComputers * 100)
#if (Test-Connection $computer -Count 1) {
# $arrayComputers.add($computer) | Out-Null
#}
Start-Sleep -Seconds 1
}
BTW the primary problem with your approach above is that the ShowDialog() method call doesn't return until the form is closed. If you want to use ShowDialog(), you have to execute your logic in the context of an event handler inside the form (Load/Activate). The other option is to use Show() but that means you'll have to pump messages in your loop using Application.DoEvents() so the form will have a chance to update itself.

Related

Exception from HRESULT: 0x800A03EC (Trying to modify cell in Excel through PowerShell)

I just recently joined an IAM team, and this month had to send out hundreds of emails to people notifying them of an account expiration (they are asked to either request for an extension or termination of the account). Thankfully, there's already a script made to do that part, but for dealing with the responses there is not. There's an excel spreadsheet where I record what is to happen to each account. I was hoping to make a script that can go through each of the responses and mark in the desired field in the spreadsheet accordingly. I've been having trouble with the part of the script where I modify the value under the desired field for the user.
I'm fairly new to PowerShell, so I'm not sure what the issue is. I already spent a few hours looking online and found quite a few possible solutions, but none of them have worked for me. A common problem is apparently using an older excel file, but it's fresh and it's Excel 2016. Another one is not having the correct file type, but I checked and that's not it either. The line of code in question is $extend.Cells.Item($modifyCell.Cells.Row) = "$data".
Any ideas what the problem could be?
Code:
# Path to .msg files
$msgDir = "C:\Users\me\Desktop\Test"
# Array to store results
$msgArray = New-Object System.Collections.Generic.List[object]
# Loop throuch each .msg file
Get-ChildItem "$msgDir" -Filter *.msg |
ForEach-Object {
# Open .msg file
$outlook = New-Object -comobject outlook.application
$msg = $outlook.Session.OpenSharedItem($_.FullName)
# Add .msg file Subject and Body to array
$msgArray.Add([pscustomobject]#{Subject=$msg.Subject;Body=$msg.Body;})
$msg.Close(0) # Close doesn't always work, see KB2633737 -- restart ISE/PowerShell
}
# Loop though / parse each message
ForEach ($message in $msgArray) {
$subject = $message.subject
$body = $message.body
$regex = [regex] '\s*(\w*)\s*\|$'
If ($body -match $regex) {
$username = $body
}
$parse = $body | Select-String -Pattern "Please extend"
If ($parse -eq "Please extend") {
$data = "Y"
}
}
# Open Excel
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $True
$OpenFile = $Excel.Workbooks.Open("C:\Users\me\Desktop\test.xlsx")
$Workbook = $OpenFile.Worksheets
$Worksheet = $Workbook.Item(1)
# Get the values for each column
$samacctname = $Worksheet.Cells | where {$_.value2 -eq "SAM Account Name"} | select -First 1
$extend = $Worksheet.Cells | where {$_.value2 -eq "Extend"} | select -First 1
# Get the values for each row in SAM Account Name
$userValues = #()
for($i=2; $samacctname.Cells.Item($i).Value2 -ne $null; $i++ ){
$userValues += $samacctname.Cells.Item($i)
}
# Get the values where the cell value of SAM Account matches the username
$modifyCell = $userValues | where {$_.Value2 -eq $username}
# Modify the Extend cell using the username's row position
$extend.Cells.Item($modifyCell.Cells.Row) = "$data"
# Save the file
$OpenFile.Save()
Edit 1: I went back into my code and first tried to hard-code the data value I was trying to add to the cell, but I still got the same error. I then tried hard-coding it right when I call the line $extend.Cells.Item($modifyCell.Cells.Row) = "Y" and it works as it should. So how I'm trying to use regex to pull the username is likely not right. Probably how I'm pulling the data as well.

Update a WPF GUI using Powershell Jobs

I've been trying to create responsive GUIs for my personal Powershell scripts. I've come up with a problem that is highly discussed online: Freezing GUI (since Powershell is single threaded).
Similar to this problem, but my case is specific to Powershell. I successfully implemented a Powershell based solution for creating GUIs relying on XAML form. Now, let's consider this code:
#EVENT Handler
$Somebutton.add_Click({
$SomeLabel.Content = "Calculating..."
Start-Job -ScriptBlock {
#Computation that takes time
#...
$SomeLabel.Content = "Calculated value"
}
})
#Show XAML GUI
$xamlGUI.ShowDialog() | out-null
xamlGUI is the form itself and $Somebutton/$SomeLabel are controls I was able to read from xaml and transform to Powershell variables.
I'm trying to understand why the Job that I start is not updating my label when the computation is done. It actually does nothing.
Im new to Powershell jobs and I'm wondering if there is something I'm missing.
Here's a little boilerplate I use for reactive WPF forms in PowerShell:
# Hide yo console
$SW_HIDE, $SW_SHOW = 0, 5
$TypeDef = '[DllImport("User32.dll")]public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);'
Add-Type -MemberDefinition $TypeDef -Namespace Win32 -Name Functions
$hWnd = (Get-Process -Id $PID).MainWindowHandle
$Null = [Win32.Functions]::ShowWindow($hWnd,$SW_HIDE)
# Define your app + form
Add-Type -AssemblyName PresentationFramework
$App = [Windows.Application]::new() # or New-Object -TypeName Windows.Application
$Form = [Windows.Markup.XamlReader]::Load(
[Xml.XmlNodeReader]::new([xml]#'
WPF form definition goes here
'#)
)
# or ::Load((New-Object -TypeName Xml.XmlNodeReader -ArgumentList ([xml]#'
#wpfdef
#'#))
#)
# Fixes the "freeze" problem
function Update-Gui {
# Basically WinForms Application.DoEvents()
$App.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]::Background, [action]{})
}
# Event handlers go here
$Form.add_Closing({
$Form.Close()
$App.Shutdown()
Stop-Process -Id $PID # or return your console: [Win32.Functions]::ShowWindow($hWnd,$SW_SHOW)
})
# Finally
$App.Run($Form)
Remember to clean up when your app is shutting down:
$Form.Close()
$App.Shutdown()
Stop-Process -Id $PID
Whenever you need your changes to the GUI to be reflected, call the Update-Gui function.

Import Excel, Export CSV with PowerShell

Due to restrictions I either need to use VB or PowerShell for this task.
I have an Excel that looks like:
ColumA, ColumB,ColumC,ColumD,ColumE,ColumF
000|Txt,MoreTxt , ColumB,ColumC,ColumD,ColumE,ColumF
I read about import_csv -header, but I'm under to successfully do it. I'll post my script below. The export I expect is:
ColumA, ColumB, ColumC, ColumD, ColumE, ColumF
000, ColumB, ColumC, ColumD, ColumE, ColumF
Only Colum gets modified, and I -only- need the digits from before that pipe. It also has to stay three digits, so 1 becomes 001, etc.
This is the script I modified based on some previous inquiries I saw, and the MS Tutorial.
$file = import-csv "C:\path\to\my\file\test.csv"
foreach ($row in $file){
$tempfile = New-Object psobject -Property #{
ColumA = $row. 'ListName'.substring(0,2)
ColumB = $row. 'ColumB'
ColumC = $row. 'ColumC'
ColumE = $row. 'ColumE'
ColumF = $row. 'ColumF'
}
$expandfile = #()
$expandfile += $tempfile | select ColumA, ColumB, ColumC, ColumD, ColumE, ColumF
}
PS gives me both errors on not liking everything I have in quotes (Which I thought was the column name, but I guess not. And also a parse error on the entire array. Essentially the entire script.
UPDATE
Providing real examples of source.
"Tiam
Name",SiamName,Siam,Ciam,Piam,Liam,Niam,Diam
"002|City, State","City, State - Some text (15092)",1,"3,408",99,"3,408",780,22.89%
"009|City, State","City, State - Some Text (E) (15450)",1,"1,894",81,"1,894",543,28.67%
Edit:
$expandfile = Import-Csv "C:\path\to\my\file\test.csv" | ForEach-Object {
$_."Tiam`r`nName" = $_."Tiam`r`nName".SubString(0,3)
$_
}

SelectedNode Property Of TreeView Object Hangs The Form Multithreading

I am working on a GUI script and I ran into a problem with my TreeView object. The problem occurs when I try to display the SelectedNode property or try to use the GetNodeAt() method of a TreeView that is displayed in another thread. I am able to display the TreeView object itself though. This is a sample of what I'm trying to do:
$form = new-object system.windows.forms.form
$treeview = new-object system.windows.forms.treeview
$treeview.name = "tree view"
$treeview.add_afterselect({write-host $this.selectednode})
$treenode = new-object system.windows.forms.treenode
$treenode.name = "tree node"
$treenode.text = "tree node"
$treenode2 = new-object system.windows.forms.treenode
$treenode2.name = "second tree node"
$treenode2.text = "second tree node"
$treeview.nodes.addrange(#($treenode,$treenode2))
$form.controls.add($treeview)
$thread = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$thread.Open()
$thread.SessionStateProxy.SetVariable("form",$form)
$thepipe = $thread.CreatePipeline({$form.Showdialog()})
$thepipe.Input.Close()
$thepipe.InvokeAsync()
The issue comes with the line $treeview.add_afterselect({write-host $this.selectednode}) When I click a node in the treeview the form will hang and I have to kill the Powershell process. The weird thing is I can display the TreeView object with no issue. Like this: $treeview.add_afterselect({write-host $this}). This will return the TreeView Object with no issues. I have also tried to use this instead $treeview.add_nodemouseclick({write-host $this.getnodeat($_.x,$_.y)}). This will also hang the form.
The problem comes from the form being displayed in another thread because when I do the same code but keep the form in the same thread I have no issues with displaying the selected node. like this:
$form = new-object system.windows.forms.form
$treeview = new-object system.windows.forms.treeview
$treeview.name = "tree view"
$treeview.add_afterselect({write-host $this.selectednode})
$treenode = new-object system.windows.forms.treenode
$treenode.name = "tree node"
$treenode.text = "tree node"
$treenode2 = new-object system.windows.forms.treenode
$treenode2.name = "second tree node"
$treenode2.text = "second tree node"
$treeview.nodes.addrange(#($treenode,$treenode2))
$form.controls.add($treeview)
$form.Showdialog()
Any help will be greatly appreciated. Thank you!
If you take a look at the TreeViewEventHandler method signature, you'll find that it takes two arguments, a sender and a TreeViewEventArgs object.
You can either "intercept" these by declaring your own named parameters, like so:
$treeview.add_AfterSelect({
param($s,$e)
# $e now refers to the TreeViewEventArgs
Write-Host $e.Node
})
Or you can rely on the automatic event variables $Sender and $EventArgs:
$treeview.add_AfterSelect({
Write-Host $EventArgs.Node
})
You may want to have a look at the help files for Register-ObjectEvent and about_Automatic_Variables
Don't use $this inside an event action - it's meant to be used as an instance reference in script method definitions, like so:
PS C:\> $obj = New-Object psobject -Property #{ Number = 3 }
PS C:\> $obj |Add-Member -Type ScriptMethod -Name GetSquare -Value { return $this.Number * $this.Number }
PS C:\> $obj.GetSquare()
9
PS C:\> $obj.Number = 4
PS C:\> $obj.GetSquare()
16

Powershell WinForms UI refresh / live update

I'm currently pulling my hair out trying to update a Winforms UI using the 'Register-objectevent' Cmdlet.
What I'm trying to do is get the Register-ObjectEvent to update the label in the form ever tick on the timer.
I've done hours of research on this, and I know it's something to do with multithreading / invoking, but I can't get my head around how to make it work !
If someone could show me / help me to get this script to update the label on the form by the timer, that would be amazing ! I've got lots of Winforms that would benifit from multithreading, but I need to get my head around it first !
Here's the script I'm trying to get working, any help is greatly appreciated :)
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[System.Windows.Forms.Application]::EnableVisualStyles() | out-null
$form1 = New-Object System.Windows.Forms.Form
$OnLoadForm_StateCorrection=
{
$form1.WindowState = $InitialFormWindowState
}
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 600
$System_Drawing_Size.Width = 1200
$form1.ClientSize = $System_Drawing_Size
$form1.MaximizeBox = $False
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.KeyPreview = $True
$form1.FormBorderStyle = 1
$form1.Name = "form1"
$form1.StartPosition = 1
$form1.backcolor = [System.Drawing.Color]::FromArgb(255,240,240,240)
$timer = New-Object System.Timers.Timer
$timer.Interval = 1000
$timer.AutoReset = $true
$timeout = 0
$num=0
$action = {
$num++
write-host "test"
$vollabel.text=$num
$timer.stop()
}
Register-ObjectEvent -InputObject $timer -SourceIdentifier TimerElapsed -EventName Elapsed -Action $action
$timer.start()
$vollabel = New-Object System.Windows.Forms.Label
$vollabel.Location = "0,0"
$form1.Controls.Add($vollabel)
$InitialFormWindowState = $form1.WindowState
$form1.add_Load($OnLoadForm_StateCorrection)
$form1.Add_Shown({$form1.Activate()})
$form1.ShowDialog()| Out-Null
I pared down your script a bit for a working proof of concept - add back in anything you needed:
#('System.Drawing','System.Windows.Forms') | %{ [reflection.assembly]::LoadWithPartialName($_) | Out-Null }
[System.Windows.Forms.Application]::EnableVisualStyles() | out-null
$form1 = New-Object System.Windows.Forms.Form -Property #{
MaximizeBox = $False
KeyPreview = $True
FormBorderStyle = 1
Name = "form1"
StartPosition = 1
backcolor = [System.Drawing.Color]::FromArgb(255,240,240,240)
ClientSize = New-Object System.Drawing.Size -Property #{Height = 600;Width = 1200}
}
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$timer = New-Object System.Windows.Forms.Timer -Property #{Interval = 1000} #Forms.Timer doesn't support AutoReset property
$script:num=0 #scope must be at script level to increment in event handler
$timer.start()
$timer.add_Tick({
$script:num +=1
write-host "test $script:num"
$vollabel.text=$script:num
})
$vollabel = New-Object System.Windows.Forms.Label -Property #{Location = "0,0"}
$form1.Controls.Add($vollabel)
$form1.ShowDialog()| Out-Null
$timer.stop() #This will keep running in the background unless you stop it
A few notes:
Form.ShowDialog() is blocking and stops the script execution.
System.Windows.Forms.Timer has slightly different properties than System.Timers.Timer and can take a ScriptBlock or a function name as a parameter to add_Tick()
An event handler ScriptBlock has its own scope, but you can share variables with the $ScopeName:VariableName syntax. I couldn't get $num to increment unless I set the scope to $Script

Resources