Update a WPF GUI using Powershell Jobs - multithreading

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.

Related

How to create a function in powershell that runs a macro in an Excel workbook?

How do I create a function in powershell that runs a macro in an Excel workbook?
I've tried the following:
Function xlRunMacro($excel, $macroname){
try{
$excel.Run($macroname)
}catch{
echo $error
$key = 'Open-Excel';
$date = Get-Date -Format 'yyyy-MM-dd';
Log-Error $key $date;
$error.Clear()
}
}
$macroname = "'delete_old'"
$setexcel = New-Object -ComObject Excel.Application
$currentworkbook = $setexcel.Workbooks.Open("${paths}${files}")
xlRunMacro($setexcel, $macroname)
However, I get this error:
Method invocation failed because [System.String] does not contain a method named 'Run'.
Any ideas please?
As #Lee_Dailey mentioned, params need to be separated.
To be on the safe side generally, it's a good idea for functions to accept params by name rather than position.
e.g.
Function xlRunMacro{
param(
$excel,
[string]$macroname
)
$excel.Run($macroname)
}
xlRunMacro -excel $setexcel -macroname $macroname

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.

How to manage MS Dynamics NAV C/AL Codes in TFS with versioning

I need to keep all MS Dynamics NAV development codes in TFS, currently I export NAV objects in FOB/TXT format and add these to TFS. But I am not able to leverage the advantages of TFS, like versioning, check-in, check-out process etc., so its unmanageable between development team and QA team.
As MS Dynamics NAV keep their Codes in SQL database, I have only option to export the objects and manually add in TFS.
Experts, please advice me how could I manage these all in TFS, like we manage Visual Studio projects.
Thanks
Kishore
There isn't any native options for source control in Dynamics NAV at this point (how amazing Git integration would be!), unless you manually export text files...
There are however some paid developments such as Object Manager that do integrate with TFS and VSS. They have a concept of projects which I believe map to branches and so on. IDYN have a trial, so it might be worth downloading it and trying it out first, as it's still a bit shaky for 2013.
I'm a bit of a noob to NAV development, and was unable to find a good ready-made way to get all objects that I have permission to export out into text files that could be meaningfully source-controlled, so I wrote this powershell script that attempts to export all of them and just moves on when it gets the permission error.
#### change these params to point this script at a different server or different directory ####
$serverName = "server_where_nav_db_lives"
$dbname = "name_of_your_nav_db"
$destFolder = "c:\TestNavExport"
$startTime = [System.DateTime]::Now
function doExport {
param ([string]$type, [string]$id, [string]$name, [string]$svr, [string]$db, [string]$dest)
$cleanName = $name.replace("/", "")
$filepath = "`"{0}\{1}s\{2} - {3}.txt`"" -f $dest, $type, $id, $cleanName
write-host "filepath is $filepath"
### change this value to use a different version of the dev environment
$finpath = "c:\program Files (x86)\Microsoft Dynamics NAV\71\RoleTailored Client - CU25\finsql.exe"
$args = "command=exportobjects, file=$filepath, servername=$svr, database=$db, ntauthentication=yes, logfile=`"$dest\exportlog-$type-$cleanName.txt`", filter=`"Type=$type;ID=$id`" "
Start-Process $finpath -ArgumentList $args -Wait
}
function CheckExists {
param ([string]$pth)
if (-Not [System.IO.Directory]::Exists($pth)) { md -Path $pth }
}
$qry = #"
SELECT
o.ID,
o.[Name],
o.[Type],
CASE o.[Type] WHEN 0 THEN 'Table'
WHEN 3 THEN 'Report'
WHEN 5 THEN 'CodeUnit'
WHEN 6 THEN 'XMLPort'
WHEN 7 THEN 'MenuSuite'
WHEN 8 THEN 'Page'
WHEN 9 THEN 'Query'
ELSE 'Other'
END ObjType
FROM [Object] o
WHERE o.[Type] IN (0, 3, 5, 6, 7, 8, 9)
AND o.[ID] < 150
ORDER BY o.[ID]
"#
# make sure all the necessary directories exist
CheckExists $destFolder
CheckExists "$destFolder\Tables"
CheckExists "$destFolder\CodeUnits"
CheckExists "$destFolder\Pages"
CheckExists "$destFolder\Reports"
CheckExists "$destFolder\XMLPorts"
CheckExists "$destFolder\MenuSuites"
CheckExists "$destFolder\Querys"
# query the database for all of the objects
$connection = new-object system.Data.SqlClient.SqlConnection("Server=$serverName;Database=$dbname;Trusted_Connection=True")
$connection.Open();
$cmd = $connection.CreateCommand();
$cmd.CommandText = $qry;
$cmd.Connection = $connection;
$reader = [System.Data.SqlClient.SqlDataReader]$cmd.ExecuteReader()
while ($reader.Read()) {
$cid = $reader["ID"];
$cname = $reader["Name"]
$ctype = $reader["Type"]
$cTypeName = $reader["ObjType"]
write-host "exporting $cname $cid $ctypename"
doExport $cTypeName $cid $cname $serverName $dbname $destFolder
}
$connection.Close()
$endTime = [System.DateTime]::Now
("start time {0}, end time {1}, span {2}" -f $startTime, $endTime, ($endTime - $startTime)) | out-file "$destFolder\clock.txt"
I also found this other question, which shows how to get that text out of the object metadata table. This article has sample code that looks like it could translate the blobs in the metadata table into the text. Having written the script and verified that it works, I've stuck with the script so far. As a noob, I will not be too surprised if I find a much more efficient solution soon.
For some time we've used Visual Studio plug-in. It worked for VS2008 and NAV up to version 5.
It allowed to import and export code from/to Nav by pressing buttons in VS as well as check-in/check-out.
I hope this link still works.
But I must say it was not really a convenient solution and it lacked some important features for source code management.
You can also look at third-party solutions like ToIncreaseSolutions and ifacto

Powershell 1.0 Excel Automation - Problem with "Font.ColorIndex"

I’m trying to automate Excel in Powershell 1.0 and am having problems trying to apply a cells “Font.ColorIndex” property.
The following Microsoft KB article details a BUG with Excel automation when the computer running the script has a locale setting other than “en-us”
My example script below works perfectly when I manually change my locale and regional settings to "en-us" and fails in the last line only when set to "en-gb"
$Excel = New-object -com Excel.Application
$culture = [System.Globalization.CultureInfo]'en-us'
$Book = $Excel.Workbooks.psbase.gettype().InvokeMember("Add",
[Reflection.BindingFlags]::InvokeMethod,
$null, $Excel.Workbooks, $null, $culture)
$Sheet = $Book.Worksheets.Item(1)
$Excel.Visible = $True
$Sheet.Cells.Item(1,1).FormulaLocal = "test"
$Sheet.Cells.Item(1,1).Font.ColorIndex = 3
As previously stated, If my locale is set to “en-gb” the script works fine until the last line where it fails with:
Property 'ColorIndex' cannot be found on this object; make sure it exists and is settable.
At :line:10 char:29
+ $Sheet.Cells.Item(1,1).Font. <<<< ColorIndex = 3
Does anyone have any ideas how to resolve this (other than setting my locale to “en-us” of course!!)
Thanks
-Mark
It appears from the KB article that the workarounds all involve setting the culture to en-US unless you want to install the MUI for Office on your PC. The good news is that you can temporarily set the culture to en-US in your script for the problematic code. The following script is something the PowerShell team posted a long time ago but is still handy:
Function Using-Culture (
[System.Globalization.CultureInfo]$culture = `
(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"),
[ScriptBlock]$script= `
(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"))
{
$OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
trap
{
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
}
[System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
Invoke-Command $script
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
}
Execute the last line like so and see if it works:
Using-Culture en-US { $Sheet.Cells.Item(1,1).Font.ColorIndex = 3 }

Resources