Child thread Manipulating Parent Thread - multithreading

The original script/tool so far is 1000+ lines so I made a mock that represents my problem.
What this code basically does is create a Parent Thread with a GUI and its Start Button creates Child Threads that copies and executes files on remote computers. I want those Child Threads to update the Parent Thread's textbox with results. In the code, I've marked my attempts.
I've read about creating the GUI in a child thread and manipulating it through the parent thread but if I were to do that, I think I would have to move all the Functions and Events, that pertain to the GUI, to that child thread. And then the Start button (Child Thread Creator) would create a 2nd Gen Child Thread? It just put me in a loop, so I couldn't use that method. Or I could have the Child GUI thread somehow initiate processes in the Parent Thread? idk... is it possible to create a GUI's functions/events in the parent thread but executed from the child thread? idk...
Please provide any advice or solutions that I can consider to accomplish this.
EDIT: To recap the problem is that the child threads do not update the parent threads textbox.
#GUI
[XML]$XAML =
#"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Window" Title="Some tool you've never heard of" Height="450" Width="450" ResizeMode="CanMinimize">
<Grid>
<TextBox Name="Results" IsReadOnly="True" Margin="5,5,5,75" TextWrapping="Wrap" Background="Black" Foreground="White"/>
<Button Name="Start" Content="Start" Margin="5,350,5,5" Cursor="Hand"/>
</Grid>
</Window>
"#
[System.Void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$GUI = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $XAML))
ForEach ($Node in ($XAML.SelectNodes("//*[#Name]")))
{
Set-Variable -Name "$($Node.Name)" -Value $GUI.FindName($Node.Name) -Scope Global
}
#Sync Hash Table - Attempt 2
$Global:SyncUIHash = [hashtable]::Synchronized(#{})
$Global:SyncUIHash.Window = $Window
$Global:SyncUIHash.Results = $Results
#RunSpacePool
$RunSpacePool = [RunSpaceFactory]::CreateRunspacePool(1,5)
$RunSpacePool.ApartmentState = "STA"
$RunSpacePool.Open()
#GUI Events
$Start.Add_Click({Start-Function})
#Functions
Function Start-Function
{
($PowerShell = [Powershell]::Create()).RunSpacePool = $RunspacePool
$Parameters = #{
Computer = "ComputerName"
FileToExecute = "FilePath to file that will be executed"
Arguments = "Arguments to be used on FileToExecute"
TempDir = "Remote Computer's TempDir to Copy To"
Results = $Results #Does not Work - Attempt 1
SyncUIHash = $Global:SyncUIHash #Does not work - Attempt 3
}
[System.Void]$PowerShell.AddScript({
Param (
$Computer,
$FileToExecute,
$Arguments,
$TempDir,
$Results, #Does not work - Attempt 1
$SyncUIHash #Does not work - Attempt 3
)
#Basically: Copy File to Remote Computer then Execute File
$Results.Text += "Results of whole process." #Does not work - Attempt 1
$SyncUIHash.Results.Text += "Results of whole process." # Does not work - Attempt 2 & 3
}).AddParameters($Parameters)
$PowerShell.BeginInvoke()
}
#Shows GUI
[System.Void] $GUI.ShowDialog()

Related

Dispatcher.InvokeAsync() locks up the GUI until task is finished

I have a control (type DataGrid) that holds large amount of data (up to 1M rows). I implemented a copy-to-clipboard method that dumps the entire contents to the clipboard. This usually takes up to 2 to 3 minutes.
To avoid my users waiting for the process, I'd like to use another thread to process the copy method and return the control back to GUI immediately so the users can proceed with other tasks. When the copy method is complete, then pop up a message box to inform the user.
However, when the code is executed, the GUI gets locked up and does not responds to any user actions until the entire copy method is completed. I wonder where I got it wrong.
Here is the code snappet:
private void Copy_To_Clipboard()
{
this.gridView.ClipboardCopyMode = DataGridClipboardCopyMode.IncludeHeader;
DataGridSelectionUnit u = this.gridView.SelectionUnit;
this.gridView.SelectionUnit = DataGridSelectionUnit.CellOrRowHeader;
this.gridView.SelectAll();
ApplicationCommands.Copy.Execute(null, this.gridView);
this.gridView.UnselectAll();
this.gridView.SelectionUnit = u;
}
private async void cmdClipboard_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("The data is being copied to clipboard. This can take a while as there are " +
this.NumRecords.ToString("###,###,##0") + " records to be copied. You will be notified when it’s finished",
"Copy to Clipboard", MessageBoxButton.OK, MessageBoxImage.Information);
await this.Dispatcher.InvokeAsync((Action)(() =>
{
Copy_To_Clipboard();
}));
MessageBox.Show("Copy to clipboard is completed.", "Copy to Clipboard", MessageBoxButton.OK, MessageBoxImage.Information);
}
InvokeAsync doesn't mean "run this code on a background thread"; it means "run this code on the UI thread". Since the code calling InvokeAsync is already on the UI thread, that call does exactly nothing helpful.
The problem is that UI operations must run on the UI thread. This includes reading all the grid values and writing to the clipboard (which is considered a UI object on Windows).

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.

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 GUI, Progress Bar, and Hash Tables

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.

Speed up reading an Excel File in Powershell

I wonder if there is any way to speed up reading an Excel file with powershell. Many would say I should stop using the do until, but the problem is I need it badly, because in my Excel sheet there can be 2 rows or 5000 rows. I understand that 5000 rows needs some time. But 2 rows shouldn't need 90sec+.
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $true
$Excel.DisplayAlerts = $false
$Path = EXCELFILEPATH
$Workbook = $Excel.Workbooks.open($Path)
$Sheet1 = $Workbook.Worksheets.Item(test)
$URows = #()
Do {$URows += $Sheet1.Cells.Item($Row,1).Text; $row = $row + [int] 1} until (!$Sheet1.Cells.Item($Row,1).Text)
$URows | foreach {
$MyParms = #{};
$SetParms = #{};
And i got this 30 times in the script too:
If ($Sheet1.Cells.Item($Row,2).Text){$var1 = $Sheet1.Cells.Item($Row,2).Text
$MyParms.Add("PAR1",$var1)
$SetParms.Add("PAR1",$var1)}
}
I have the idea of running the $MyParms stuff contemporarily, but I have no idea how. Any suggestions?
Or
Increase the speed of reading, but I have no clue how to achieve that without destroying the "read until nothing is there".
Or
The speed is normal and I shouldn't complain.
Don't use Excel.Application in the first place if you need speed. You can use an Excel spreadsheet as an ODBC data source - the file is analogous to a database, and each worksheet a table. The speed difference is immense. Here's an intro on using Excel spreadsheets without Excel
Appending to an array with the += operator is terribly slow, because it will copy all elements from the existing array to a new array. Use something like this instead:
$URows = for ($row = 1; !$Sheet1.Cells.Item($row, 1).Text; $row++) {
if ($Sheet1.Cells.Item($Row,2).Text) {
$MyParms['PAR1'] = $Sheet1.Cells.Item($Row, 2).Text)
$SetParms['PAR1'] = $Sheet1.Cells.Item($Row, 2).Text)
}
$Sheet1.Cells.Item($Row,1).Text
}
Your Do loop is basically a counting loop. The canonical form for such loops is
for (init counter; condition; increment counter) {
...
}
so I changed the loop accordingly. Of course you'd achieve the same result like this:
$row = 1
$URows = Do {
...
$row += 1
}
but that would just mean more code without any benefits. This modification doesn't have any performance impact, though.
Relevant in terms of performance are the other two changes:
I moved the code filling the hashtables inside the first loop, so the code won't loop twice over the data. Using index and assignment operators instead of the Add method for assigning values to the hashtable prevents the code from raising an error when a key already exists in the hashtable.
Instead of appending to an array (which has the abovementioned performance impact) the code now simply echoes the cell text in the loop, which PowerShell automatically turns into a list. The list is then assigned to the variable $URows.

Resources