I am writing a Powershell script utilizing WPF with the methods shown here: http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/.
I'm encountering an issue with the follow code freezing the UI as the "btnTest" button is clicked. I'm guessing this is some sort of deadlock because one thread is attempting to access another thread's resources. I hope there's some way around this:
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$global:syncHash = [Hashtable]::Synchronized(#{})
$syncHash.Host = $Host
$runSpace = [RunspaceFactory]::CreateRunspace()
$runSpace.ApartmentState,$runSpace.ThreadOptions = “STA”,”ReUseThread”
$runSpace.Open()
$runSpace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)
$cmd = [PowerShell]::Create().AddScript({
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Controls
$syncHash.Window = [Windows.Markup.XamlReader]::Parse(#”
XAML goes here
“#)
$syncHash.btnTest = $global:syncHash.Window.FindName(“btnTest”)
$syncHash.txtText = $global:syncHash.Window.FindName(“txtTest”)
$syncHash.btnTest.Add_Click({
$syncHash.btnTest.IsEnabled = $false
$syncHash.Host.Runspace.Events.GenerateEvent(“btnTestClicked”, $syncHash.btnTest, $null, $null)
})
$syncHash.Window.ShowDialog()
})
$cmd.Runspace = $runSpace
$handle = $cmd.BeginInvoke()
# THIS CODE FREEZES THE UI
Register-EngineEvent -SourceIdentifier “btnTestClicked” -Action {
$global:syncHash.btnTest.Dispatcher.invoke([System.Windows.Threading.DispatcherPriority]::Normal, [action]{$global:syncHash.btnTest.IsEnabled = $true}, $null, $null)
}
Any thoughts would be greatly appreciated!
Related
I am creating a PowerShell script with a GUI, that copies user profiles from a selected source disk to a destination disk. I've created the GUI in XAML, with VS Community 2019.
My copy function is called from a runspace and works fine. I would like to put a progress bar inside this runspace so that it increases for each folder copied, but I can't figure out how.
Here is my runspace :
function RunspaceBackupData {
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$Runspace.SessionStateProxy.SetVariable("SelectedFolders",$global:SelectedFolders)
$Runspace.SessionStateProxy.SetVariable("SelectedUser",$global:SelectedUser)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskSource",$global:ReturnedDiskSource)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskDestination",$global:ReturnedDiskDestination)
$code = {
foreach ($item in $global:SelectedFolders) {
copy-item -Path "$global:ReturnedDiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
}
}
$PSinstance = [powershell]::Create().AddScript($Code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()
}
Here is my event-handler :
$var_btnStart.Add_Click( {
RunspaceBackupData -syncHash $syncHash -SelectedFolders $global:SelectedFolders -SelectedUser $global:SelectedUser -ReturnedDiskSource $global:ReturnedDiskSource -ReturnedDiskDestination $global:ReturnedDiskDestination
})
Can you please help me ?
Ok i've managed to get my progress bar working. First you gotta add it to your hashtable variable :
$syncHash.ProgressBar = $syncHash.Window.FindName("ProgressBar")
Then you can create your event-handler like this :
$var_btnStart.Add_Click( {
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$Worker =
[PowerShell]::Create().AddScript({$syncHash.ProgressBar.Dispatcher.Invoke([action]
{$SyncHash.ProgressBar.IsIndeterminate = $true }, "Normal")}
##YOUR CODE HERE
$syncHash.Error = $Error
$syncHash.ProgressBar.Dispatcher.Invoke([action]{$SyncHash.ProgressBar.IsIndeterminate = $false }, "Normal")
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()})
Note that in this example your progress bar is indeterminate, so it starts looping when you click on the button, and stops when you code ends.
Don't forget to dispose your runspace then :
$Runspace.PowerShell.Close($Runspace) $Runspace.PowerShell.Dispose($Runspace)
Inside $code you need to keep the count, so I suggest a for-loop.
Inside the loop you can call the dispatcher in the window-object:
$syncHash.Window.Dispatcher.Invoke([action]{$syncHash.Progressbar.Value = [double] <code for finished percentage>}, "Normal")
I suspect you have window and progressbar in $syncHash
I have the following PowerShell function which works well, but the window opens up in the background behind the PowerShell ISE.
# Shows folder browser dialog box and sets to variable
function Get-FolderName() {
Add-Type -AssemblyName System.Windows.Forms
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property #{
SelectedPath = 'C:\Temp\'
ShowNewFolderButton = $false
Description = "Select Staging Folder."
}
# If cancel is clicked the script will exit
if ($FolderBrowser.ShowDialog() -eq "Cancel") {break}
$FolderBrowser.SelectedPath
} #end function Get-FolderName
I can see there's a .TopMost property that can be used with the OpenFileDialog class but this doesn't appear to transfer over to the FolderBrowserDialog class.
Am I missing something?
Hope this helps
Add-Type -AssemblyName System.Windows.Forms
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowser.Description = 'Select the folder containing the data'
$result = $FolderBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property #{TopMost = $true }))
if ($result -eq [Windows.Forms.DialogResult]::OK){
$FolderBrowser.SelectedPath
} else {
exit
}
//Edit to comment
There are 2 variants (overloads) of the ShowDialog () method.
See documentation: http://msdn.microsoft.com/en-us/library/system.windows.forms.openfiledialog.showdialog%28v=vs.110%29.aspx
In the second variant, you can specify the window that should be the mother of the dialogue.
Topmost should be used sparingly or not at all! If multiple windows are topmost then which is topmost? ;-))
First try to set your window as a mother then the OpenfileDialog / SaveFileDialog should always appear above your window:
$openFileDialog1.ShowDialog($form1)
If that's not enough, take Topmost.
Your dialogue window inherits the properties from the mother. If your mother window is topmost, then the dialog is also topmost.
Here is an example that sets the dialogue Topmost.
In this example, however, a new unbound window is used, so the dialog is unbound.
$openFileDialog1.ShowDialog((New - Object System.Windows.Forms.Form - Property #{TopMost = $true; TopLevel = $true}))
A reliable way of doing this is to add a piece of C# code to the function.
With that code, you can get a Windows handle that implements the IWin32Window interface. Using that handle in the ShowDialog function will ensure the dialog is displayed on top.
Function Get-FolderName {
# To ensure the dialog window shows in the foreground, you need to get a Window Handle from the owner process.
# This handle must implement System.Windows.Forms.IWin32Window
# Create a wrapper class that implements IWin32Window.
# The IWin32Window interface contains only a single property that must be implemented to expose the underlying handle.
$code = #"
using System;
using System.Windows.Forms;
public class Win32Window : IWin32Window
{
public Win32Window(IntPtr handle)
{
Handle = handle;
}
public IntPtr Handle { get; private set; }
}
"#
if (-not ([System.Management.Automation.PSTypeName]'Win32Window').Type) {
Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms.dll -Language CSharp
}
# Get the window handle from the current process
# $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
# Or write like this:
$owner = [Win32Window]::new([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
# Or use the the window handle from the desktop
# $owner = New-Object Win32Window -ArgumentList (Get-Process -Name explorer).MainWindowHandle
# Or write like this:
# $owner = [Win32Window]::new((Get-Process -Name explorer).MainWindowHandle)
$FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property #{
SelectedPath = 'C:\Temp\'
ShowNewFolderButton = $false
Description = "Select Staging Folder."
}
# set the return value only if a selection was made
$result = $null
If ($FolderBrowser.ShowDialog($owner) -eq "OK") {
$result = $FolderBrowser.SelectedPath
}
# clear the dialog from memory
$FolderBrowser.Dispose()
return $result
}
Get-FolderName
You can also opt for using the Shell.Application object with something like this:
# Show an Open Folder Dialog and return the directory selected by the user.
function Get-FolderName {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Select a directory.",
[string]$InitialDirectory = [System.Environment+SpecialFolder]::MyComputer,
[switch]$ShowNewFolderButton
)
$browserForFolderOptions = 0x00000041 # BIF_RETURNONLYFSDIRS -bor BIF_NEWDIALOGSTYLE
if (!$ShowNewFolderButton) { $browserForFolderOptions += 0x00000200 } # BIF_NONEWFOLDERBUTTON
$browser = New-Object -ComObject Shell.Application
# To make the dialog topmost, you need to supply the Window handle of the current process
[intPtr]$handle = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/bb773205(v=vs.85).aspx
$folder = $browser.BrowseForFolder($handle, $Message, $browserForFolderOptions, $InitialDirectory)
$result = $null
if ($folder) {
$result = $folder.Self.Path
}
# Release and remove the used Com object from memory
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($browser) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return $result
}
$folder = Get-FolderName
if ($folder) { Write-Host "You selected the directory: $folder" }
else { "You did not select a directory." }
I just found an easy way to get PowerShell's IWin32Window value so forms can be modal. Create a System.Windows.Forms.NativeWindow object and assign PowerShell's handle to it.
function Show-FolderBrowser
{
Param ( [Parameter(Mandatory=1)][string]$Title,
[Parameter(Mandatory=0)][string]$DefaultPath = $(Split-Path $psISE.CurrentFile.FullPath),
[Parameter(Mandatory=0)][switch]$ShowNewFolderButton)
$DefaultPath = UNCPath2Mapped -path $DefaultPath;
$FolderBrowser = new-object System.Windows.Forms.folderbrowserdialog;
$FolderBrowser.Description = $Title;
$FolderBrowser.ShowNewFolderButton = $ShowNewFolderButton;
$FolderBrowser.SelectedPath = $DefaultPath;
$out = $null;
$caller = [System.Windows.Forms.NativeWindow]::new()
$caller.AssignHandle([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
if (($FolderBrowser.ShowDialog($caller)) -eq [System.Windows.Forms.DialogResult]::OK.value__)
{
$out = $FolderBrowser.SelectedPath;
}
#Cleanup Disposabe Objects
Get-Variable -ErrorAction SilentlyContinue -Scope 0 | Where-Object {($_.Value -is [System.IDisposable]) -and ($_.Name -notmatch "PS\s*")} | ForEach-Object {$_.Value.Dispose(); $_ | Clear-Variable -ErrorAction SilentlyContinue -PassThru | Remove-Variable -ErrorAction SilentlyContinue -Force;}
return $out;
}
I am new to WPF and event-handling using powershell, what I want to achieve is on click of a button, the progress bar should be shown from 0-100. But when I am running the following piece of code, it computes the whole code inside the add_click block and it only shows the last iteration in the loop.
I know maybe it is a silly solution, but I do need some help in this.
$syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$psCmd = [PowerShell]::Create().AddScript({
$XamlPath="C:\Forms\MyFormsv1.xaml"
$inputXML = Get-Content -Path $XamlPath
[xml]$Global:xmlWPF = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
#Add WPF and Windows Forms assemblies
try{
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
} catch {
Throw “Failed to load Windows Presentation Framework assemblies.”
}
$syncHash.Window=[Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $xmlWPF))
[xml]$XAML = $xmlWPF
$xmlWPF.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:synchash.Add($_.Name,$synchash.Window.FindName($_.Name) )}
function tool1_progress{
$var1=50
for($index=1 ; $index -le $var1; $index++)
{
[int]$tmpProgNum= ($index/$var1) * 100
$syncHash.tool1_pb.Value= $tmpProgNum
$syncHash.consoleOutput.Text=$index
Start-Sleep -Milliseconds 250
}
}
$syncHash.myButton.add_click({tool1_progress})
$syncHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
$tool1 is a label. $tool1_pb is a progress bar. $consoleOutput is a
text box.
I integrate WPF and PowerShell all the time. This website helped me immensely. You can't update a UI that's running on the same thread, so you need to invoke a new runspace using BeginInvoke()
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$xaml = #"YOURXAMLHERE"
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Parse($xaml )
[xml]$XAML = $xaml
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:uihash.Add($_.Name,$uihash.Window.FindName($_.Name) )}
$Global:uiHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
Then to update whatever it is you need, you would use Window.Dispatcher.Invoke
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.Window.Title = "MyWindowTitle"},"Normal")
EDIT
$Global:uiHash.Button1.Add_Click(tool1_progress)
EDIT
Throw in your XML and this will work. You have to create another runspace on top of the other in order to keep updating the textbox and progress bar. Note that synchash MUST be $Global:synchash. Functions courtesy of this post.
$Global:syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$psCmd = [PowerShell]::Create().AddScript({
$XamlPath="C:\Forms\MyFormsv1.xaml"
$inputXML = #"YOURXMLHERE"#
[xml]$Global:xmlWPF = $inputXML
#Add WPF and Windows Forms assemblies
try{
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
} catch {
Throw “Failed to load Windows Presentation Framework assemblies.”
}
$Global:syncHash.Window=[Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $xmlWPF))
[xml]$XAML = $xmlWPF
$xmlWPF.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
$Global:synchash.Add($_.Name,$Global:syncHash.Window.FindName($_.Name) )}
function Start-Runspace{
param($scriptblock)
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$global:synchash)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
}
$SB = {
$var1=50
for($index=1 ; $index -le $var1; $index++)
{
[int]$tmpProgNum= ($index/$var1) * 100
$Global:syncHash.Window.Dispatcher.Invoke([action]{$Global:Synchash.tool1_pb.value = "$tmpprognum"},"Normal")
$Global:syncHash.Window.Dispatcher.Invoke([action]{$Global:Synchash.consoleoutput.text = "$index"},"Normal")
Start-Sleep -Milliseconds 250
}
}
$Global:syncHash.myButton.add_click({Start-Runspace $SB})
$Global:syncHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
start-sleep -Milliseconds 300
I am trying to create a background worker that will scan for a change in USB devices.
I am having a hard time trying to control WMI events outside the runspace.
Basically, when I close the window (AKA "form") I would like to cancel Wait-Event. But I cannot control it out side the runspace, so it gets stuck.
Is it possible?
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form1 = New-Object System.Windows.Forms.Form
$form1.Text = "My PowerShell Form"
$form1.Name = "form1"
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 1400
$System_Drawing_Size.Height = 600
$form1.ClientSize = $System_Drawing_Size
$Global:x = [Hashtable]::Synchronized(#{})
$x.Host = $Host
$x.Flag = $true
$rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState, $rs.ThreadOptions = "STA", "ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("x",$x)
$cmd = [PowerShell]::Create().AddScript({
Register-WmiEvent -Class Win32_DeviceChangeEvent -SourceIdentifier volumeChange
do {
$retEvent = Wait-Event -SourceIdentifier volumeChange
Remove-Event -SourceIdentifier volumeChange
} while ($x.Flag) #Loop until next event
Unregister-Event -SourceIdentifier volumeChange
})
$cmd.Runspace = $rs
$handle = $cmd.BeginInvoke()
$OnClosing = {
$x.Flag = $false
$x.Host.UI.WriteVerboseLine($x.Flag)
$cmd.EndInvoke($handle)
}
$InitialFormWindowState = $form1.WindowState
#Init the OnLoad event to correct the initial state of the form
$form1.Add_Load($OnLoadForm_StateCorrection)
$form1.Add_Closing($OnClosing)
$form1.ShowDialog() | Out-Null
What I'm attempting to do is create a window using forms and add a marquee style progress bar that continuously loops while my script runs. I'm not concerned about tracking progress, it is just so the user knows that something is occurring.
Here's what I have so far:
Add-Type -AssemblyName System.Windows.Forms
$window = New-Object Windows.Forms.Form
$window.Size = New-Object Drawing.Size #(400,75)
$window.StartPosition = "CenterScreen"
$window.Font = New-Object System.Drawing.Font("Calibri",11,[System.Drawing.FontStyle]::Bold)
$window.Text = "STARTING UP"
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Point(10, 10)
$ProgressBar1.Size = New-Object System.Drawing.Size(365, 20)
$ProgressBar1.Style = "Marquee"
$ProgressBar1.MarqueeAnimationSpeed = 20
$window.Controls.Add($ProgressBar1)
$window.ShowDialog()
This draws the progress bar and the window, but I don't get the marquee animation inside the progress bar.
What am I missing?
VisualStyles must be enabled. That's why on ISE works, but doesn't on Console.
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$window = New-Object Windows.Forms.Form
$window.Size = New-Object Drawing.Size #(400,75)
$window.StartPosition = "CenterScreen"
$window.Font = New-Object System.Drawing.Font("Calibri",11,[System.Drawing.FontStyle]::Bold)
$window.Text = "STARTING UP"
$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Point(10, 10)
$ProgressBar1.Size = New-Object System.Drawing.Size(365, 20)
$ProgressBar1.Style = "Marquee"
$ProgressBar1.MarqueeAnimationSpeed = 20
$window.Controls.Add($ProgressBar1)
$window.ShowDialog()