I would like to embed WinForms in WPF used by PowerShell but cannot find good example.
Basically I would like to embed next in WPF:
[System.Windows.Forms.MessageBox]::Show("This is message","MessageBox","YesNo","Question")
I am getting error:
Unexpected 'PROPERTYELEMENT' in parse rule 'NonemptyPropertyElement ::
I am not sure how to insert elements:
("This is message","MessageBox","YesNo","Question")
PowerShell code where I am trying to embed it :
Add-type -AssemblyName PresentationFramework, PresentationCore, System.Windows.Forms, System.Drawing,WindowsFormsIntegration
$ToolboxXml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
Title="Toolbox"
WindowStartupLocation = "CenterScreen"
SizeToContent="Width"
ResizeMode="NoResize"
Topmost="True"
Height="390"
FontFamily="Arial"
WindowStyle="SingleBorderWindow">
<Grid Margin="7 0 7 7" TextOptions.TextRenderingMode="Aliased">
<WindowsFormsHost>
<WindowsFormsHost.Child>
<wf:MessageBox.show x:Name="MessageBox"/>
</WindowsFormsHost.Child>
</WindowsFormsHost>
</Grid>
</Window>
"#
$ToolboxGui = $ToolboxXml -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
#Load XAML
$Form = [Windows.Markup.XamlReader]::Parse(([string]$ToolboxGui))
[xml]$xaml = $ToolboxGui
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object {
New-Variable -Name $_.Name -Value $Form.FindName($_.Name) -Force
}
#$MessageBox = $Form.Findname("MessageBox")
$async = $Form.Dispatcher.InvokeAsync({
$Form.ShowDialog() | Out-Null
})
$async.Wait() | Out-Null
Related
I'm tryig to do a simple GUI WPF who show a progress bar and copie file in a filetree.
I'm trying to update my progress bar with a Start-job but i get the error "[System.String] doesn't not contains method named "Invoke" and i didn't know how to update with multithreading.
This script take two parameters :
-File ( contain path to a file )
-Folder (it's an array of path)
Param(
[String]$file,
[String[]]$folder
)
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
[xml]$XML = Get-Content -Raw -Path ".\Logique\Copie.xaml"
$FormXML = (New-Object System.Xml.XmlNodeReader $XML)
$WindowCopie = [Windows.Markup.XamlReader]::Load($FormXML)
$pb_PROGRESS = $WindowCopie.FindName("pb_PROGRESS")
$lbl_MESSAGE = $WindowCopie.FindName("lbl_MESSAGE")
$btn_OK = $WindowCopie.FindName("btn_OK")
$btn_OK.Add_click({
$job = Start-Job -Name "Copie" -ScriptBlock $script -ArgumentList $pb_PROGRESS,$lbl_MESSAGE,$file,$folder
})
$script = {
Param($progressBar,$message,$file,$folder)
$progressBar.Dispatcher.Invoke([Action]{$progressBar.Maximum = $folder.Count},"Normal")
foreach ($val in $folder) {
$message.Dispatcher.Invoke([Action]{$message.Content = "Copie en cours $($progressBar.Value)/$($folder.count)"},"Normal")
$progressBar.Dispatcher.Invoke([Action]{$progressBar.Value++})
[System.IO.File]::SetAttributes($val, 'Normal')
Copy-Item -Path $file -Destination $val
}
}
$WindowCopie.ShowDialog()| Out-Null
and here my xaml file :
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My WPF Interface" Height="150" Width="400">
<StackPanel>
<Label x:Name="lbl_MESSAGE" Content="My Message" HorizontalAlignment="Center" />
<ProgressBar x:Name="pb_PROGRESS" Minimum="0" Maximum="100" Width="360" Margin="17,10,16.057,10" Height="18" />
<Button x:Name="btn_OK" Content="OK" Width="100" Height="30" Margin="10,10,10,0" HorizontalAlignment="Center" />
</StackPanel>
</Window>
Thank's for the help
update
I am having trouble getting my "sync-folder" function to output to the textbox as it runs. I have tried looking over a lot of guides around the net including searching here and I'm not able to figure it out. I am hoping someone who understands this better than me can show me how to do it in my test example below:
$Global:syncHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
# Load WPF assembly if necessary
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TestApp" Height="450" Width="800">
<Grid>
<Button x:Name="runButton" Content="Run" HorizontalAlignment="Left" Margin="293,37,0,0" VerticalAlignment="Top" Width="189" Height="40"/>
<TextBox x:Name="textbox" TextWrapping="NoWrap" FontFamily="Consolas" ScrollViewer.VerticalScrollBarVisibility="Auto" IsReadOnly="True" Margin="10,137,10,10"/>
</Grid>
</Window>
"#
# Remove XML attributes that break a couple things.
# Without this, you must manually remove the attributes
# after pasting from Visual Studio. If more attributes
# need to be removed automatically, add them below.
$AttributesToRemove = #(
'x:Class',
'mc:Ignorable'
)
foreach ($Attrib in $AttributesToRemove) {
if ( $xaml.Window.GetAttribute($Attrib) ) {
$xaml.Window.RemoveAttribute($Attrib)
}
}
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
[xml]$XAML = $xaml
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | %{
#Find all of the form types and add them as members to the synchash
$syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name) )
}
$Script:JobCleanup = [hashtable]::Synchronized(#{})
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))
#region Background runspace to clean up jobs
$jobCleanup.Flag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("jobCleanup",$jobCleanup)
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)
$jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
#Routine to handle completed runspaces
Do {
Foreach($runspace in $jobs) {
If ($runspace.Runspace.isCompleted) {
[void]$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
}
}
#Clean out unused runspace jobs
$temphash = $jobs.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
$jobs.remove($_)
}
Start-Sleep -Seconds 1
} while ($jobCleanup.Flag)
})
$jobCleanup.PowerShell.Runspace = $newRunspace
$jobCleanup.Thread = $jobCleanup.PowerShell.BeginInvoke()
#endregion Background runspace to clean up jobs
#=========================================
#=========================================
#=========================================
#================= runButton =============
$syncHash.runButton.Add_Click({
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$SyncHash)
$PowerShell = [PowerShell]::Create().AddScript({
Function Update-Window {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)
# This is kind of a hack, there may be a better way to do this
If ($Property -eq "Close") {
$syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
Return
}
# This updates the control based on the parameters passed to the function
$syncHash.$Control.Dispatcher.Invoke([action]{
# This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
If ($PSBoundParameters['AppendContent']) {
$syncHash.$Control.AppendText($Value)
} Else {
$syncHash.$Control.$Property = $Value
}
}, "Normal")
}
#============== START stuff to do =================
function sync-folder {
function timestamp {
$timestamp = "$(Get-Date -f 'yyyy-MM-dd HH:mm:ss:fff')"
Write-Output "$timestamp "
}
$MainLog = "C:\ProgramData\test\Logs\Main_Log.txt"
$SyncLog = "C:\ProgramData\test\Logs\Sync_Log.txt"
$RemoteSync = "\\remote\sync\path\test"
$RobocopySource = "\\remote\location\"
$RobocopyDestination = "C:\ProgramData\test"
# Check if Log exists...
# If not exist, then create it:
If (!(Test-Path $MainLog)) {
# Create MainLog.txt
New-Item -ItemType File -Force $MainLog
# Write to log:
Write-Output "$(timestamp) Main Log File has been created." >> $MainLog
}
If (!(Test-Path $SyncLog)) {
# Create MainLog.txt
New-Item -ItemType File -Force $SyncLog
# Write to log:
Write-Output "$(timestamp) Sync Log File has been created." >> $SyncLog
}
if (Test-Path $RemoteSync) {
Write-Output "$(timestamp) Sync location is reachable. Starting sync..." >> $MainLog
Write-Output "$(timestamp) Sync location is reachable. Starting sync..." >> $SyncLog
robocopy $RobocopySource $RobocopyDestination /MIR /FFT /R:3 /W:10 /NP /NDL /UNILOG+:$SyncLog
Write-Output "$(timestamp) Sync complete. Check $SyncLog for details." >> $MainLog
Write-Output "$(timestamp) Sync complete." >> $SyncLog
exit
} else {
Write-Output "$(timestamp) Sync location is NOT reachable. Synchronization aborted..." >> $MainLog
Write-Output "$(timestamp) Exiting SYNC Task..." >> $MainLog
Write-Output "$(timestamp) Sync location NOT reachable. Synchronization aborted..." >> $SyncLog
Write-Output "$(timestamp) Exiting SYNC Task..." >> $SyncLog
exit
}
}
sync-folder
#============== END stuff to do ===================
})
$PowerShell.Runspace = $newRunspace
[void]$Jobs.Add((
[pscustomobject]#{
PowerShell = $PowerShell
Runspace = $PowerShell.BeginInvoke()
}
))
})
$syncHash.Window.Add_Closed({
Write-Verbose 'Halt runspace cleanup job processing'
$jobCleanup.Flag = $False
#Stop all runspaces
$jobCleanup.PowerShell.Dispose()
})
$syncHash.Window.ShowDialog() | Out-Null
$syncHash.Error = $Error
})
#=====================================
# Shows the form
#=====================================
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
I am building a little UI that will reboot a Windows machine. I would like to have the restart button prompt the user for confirmation, then perform the reboot when the second click happens. I am thinking I'll use a counter to achieve this goal, but it doesn't look like the button is updating the value of $counter. Is this the best way to achieve this?
Function Start-guitest {
[CmdletBinding()]
[xml]$inputXML = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
Title="Configuration Script" Height="187.481" Width="400">
<Grid>
<GroupBox x:Name="Actions" Header="Actions" HorizontalAlignment="Left" Height="108" VerticalAlignment="Top" Width="170" Margin="7,11,0,0">
<StackPanel>
<Button x:Name="eventLog_btn" Content="Show errors and warnings"/>
<Label />
<Button x:Name="restart_btn" Content="Restart Windows"/>
</StackPanel>
</GroupBox>
<GroupBox x:Name="Output" Header="Output" HorizontalAlignment="Left" Margin="183,11,0,0" Width="200" Height="108" VerticalAlignment="Top">
<TextBox x:Name="output_txtbx" IsReadOnly="True" HorizontalScrollBarVisibility="Auto" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" />
</GroupBox>
</Grid>
</Window>
"#
Set-Variable -Name $inputXML -Value ($inputXML.Window -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window') -Force -ErrorAction SilentlyContinue
[void][System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework')
[xml]$xaml = $inputXML
# Read XAML.
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
Try {
$form = [Windows.Markup.XamlReader]::Load($reader)
}
Catch {
#error message
}
# Connect to controls.
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name "WPF$($_.Name)" -Value $form.FindName($_.Name) -Force
}
$counter = 0
$WPFeventLog_btn.Add_Click({
Try {
Get-EventLog -LogName Application -After $startTime.AddSeconds(-3) -EntryType Warning,Error -Source $EventLogSource | Out-GridView -Title "Deployment errors.";
$WPFoutput_txtbx.Text = "Please remediate deployment errors, then re-run the config script."
}
Catch {
$WPFoutput_txtbx.Text = "There are no configuration errors or warnings."
}
})
$WPFrestart_btn.Add_Click({
Try {
if ($counter -eq 0) {
$WPFoutput_txtbx.Text = "Are you sure the server is fully configured? Counter is: $counter"
$counter++
} elseif ($counter -gt 0) {
$WPFoutput_txtbx.Text = "Okay, rebooting. Counter is: $counter"
}
}
Catch {
Write-Warning $_
}
})
$form.ShowDialog() | Out-Null
}
You are running into scope issues. Change all references of $counter to $global:counter and your issue will be resolved.
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 have a little WPF Powershell GUI with a timer:
##############################################
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$sync = [hashtable]::Synchronized(#{})
##############################################
##############################################Start form
$Form = New-Object System.Windows.Forms.Form
$Form.Size = New-Object System.Drawing.Size(600,400)
$Form.Text = "Testor"
$Form.MaximizeBox = $false
############################################## Start functions
Function Netchk {
$wlanchk1 = netsh wlan show interfaces | Select-String '\sSSID'
if ($wlanchk1 -ne $null){$wlanchk = $wlanchk1 -replace ('\s','')
$wlanchk -replace 'SSID:','Connected to: '}else{$wlanchk = "No wlan connected"}
$outputBox.Text = $wlanchk
}
############################################## end functions
############################################## Start group box
$groupBox = New-Object System.Windows.Forms.GroupBox
$groupBox.Location = New-Object System.Drawing.Size(10,10)
$groupBox.Autosize = $true
$groupBox.text = "Groupbox: "
$Form.Controls.Add($groupBox)
############################################## end group box
############################################## Start buttons
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = new-object System.Drawing.Point(15,25)
$Button1.Size = New-Object System.Drawing.Size(200,30)
$Button1.Text = "Button1"
$groupBox.Controls.Add($Button1)
$Button1.Add_click({netchk})
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Location = new-object System.Drawing.Point(15,55)
$Button2.Size = New-Object System.Drawing.Size(200,30)
$Button2.Text = "Button2"
$groupBox.Controls.Add($Button2)
$Button2.Add_click({})
$Button3 = New-Object System.Windows.Forms.Button
$Button3.Location = new-object System.Drawing.Point(15,85)
$Button3.Size = New-Object System.Drawing.Size(200,30)
$Button3.Text = "Button3"
$groupBox.Controls.Add($Button3)
$Button3.Add_click({})
$Button4 = New-Object System.Windows.Forms.Button
$Button4.Location = new-object System.Drawing.Point(15,115)
$Button4.Size = New-Object System.Drawing.Size(200,30)
$Button4.Text = "Button4"
$groupBox.Controls.Add($Button4)
$Button4.Add_click({})
############################################## end buttons
############################################## Start text field
$outputBox = New-Object System.Windows.Forms.TextBox
$outputBox.Location = New-Object System.Drawing.Size(10,200)
$outputBox.Size = New-Object System.Drawing.Size(565,150)
$outputBox.MultiLine = $True
$outputBox.ScrollBars = "Vertical"
$outputBox.Text = 0
$Form.Controls.Add($outputBox)
$Form.Controls.AddRange(#($sync.Textbox))
############################################## end text field
############################################## start label
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
$label1 = New-Object System.Windows.Forms.Label
$label1.Location = New-Object Drawing.Point (385, 30)
$label1.Width = 100
$label1.Height = 60
$label1.Text = 0
$label1.Font = New-Object System.Drawing.Font("Courier New",32,1,2,0)
############################################## end label
############################################## start timer
$timer1 = New-Object System.Windows.Forms.Timer
$timer1.Interval = 1000
$timer1.Enabled = $true
$time = 60
$script:StartTime = (Get-Date).AddSeconds($Time)
$timer1_OnTick = {
[TimeSpan]$span = $script:StartTime - (Get-Date)
$label1.Text = '{0:N0}' -f $span.TotalSeconds
if($span.TotalSeconds -le 0)
{
$timer1.Stop()
$timer1.enabled = $false
function1
$Form.Close()
stop-process -Id $PID
}
}
$timer1.add_tick($timer1_OnTick)
$Form.Controls.AddRange(#($sync.Timer))
##############################################
$sync.button1 = $button1
$sync.button2 = $button2
$sync.button3 = $button3
$sync.button4 = $button4
$sync.label1 = $label1
$sync.TextBox = $outputBox
$sync.Groupbox = $groupBox
$sync.Timer = $timer1
$Form.Controls.AddRange(#($sync.button1, $sync.button2, $sync.button3, $sync.button4, $sync.label1, $sync.TextBox, $sync.Groupbox ))
$Form.Add_Shown({$Form.Activate()})
[void] $Form.ShowDialog()
Because I'm new to powershell, I still cannot understand the runspaces launch method etc. How can I run the timer from its own runspace and how can I add functions to buttons? I need something {click} --> {open new runspace} --> {run function} while timer is still ticking in isolation. The netchk function is a simple task, what i want GUI to do.
I want to understand this =) Please, explain it to me.
Ok so your part of the way there, having created a synchronized hash table and set up a gui(although you should mark your hash table as a global variable like so
$GLOBAL:sync, all you really need to do is go ahead and create the runspace and then invoke it, which I'll show below.
First you'll need to wrap whatever code needs to be run in a separate runspace inside of a script block
$SB = {YOUR CODE HERE}
Next you need to create a new runspace
$newRunspace =[runspacefactory]::CreateRunspace()
Then You'll want to set some options, the apartment state is required for WPF applications(not sure about the WinForms you are using) and the Thread Options are recommended for performance.
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
Now the runspace must be opened
$newRunspace.Open()
Now you can add your syncronized hash table to the runspace, this command will make it accessible via the variable $syncHash inside your code.
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$sync)
After the runspace is created you'll also need to create a new powershell object and add your script. You can also add arguments here if you want but I find it easier to save whatever data is needed inside the synced hash table and using it from there.
$psCmd = [PowerShell]::Create().AddScript($SB)
Then you need to associate the runspace with the powershell object
$psCmd.Runspace = $newRunspace
and finally begin invoking the newly created object
$psCMD.BeginInvoke()
All together it looks like this
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$HashTable)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
So in order to have the runspaces interact we need to use a global synced hash table, so set your existing $sync like this
$GLOBAL:sync = [hashtable]::Synchronized(#{})
Once you have that you'll need to add the code you currently have in your netchk function to a script block, which will look like so
$SB = {
$wlanchk1 = netsh wlan show interfaces | Select-String '\sSSID'
if ($wlanchk1 -ne $null){$wlanchk = $wlanchk1 -replace ('\s','')
$wlanchk -replace 'SSID:','Connected to: '}else{$wlanchk = "No wlan connected"}
$GLOBAL:sync.txtbox.Dispatcher.Invoke([action]{$GLOBAL:outputBox.Text = $wlanchk}, "Normal")
}
Don't worry about the dispatcher stuff right now, we'll get to that in a second. After you have your scriptblock created you'll need to start your runspace when the button is clicked, to do that I've created a Start-Runspace function for you that encapsulates the code above.
function Start-Runspace{
param($scriptblock)
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$global:sync)
$psCmd = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd.Runspace = $newRunspace
$psCMD.BeginInvoke()
}
You just need to pass it your scriptblock as a parameter and it will create and run your async runspace for you. You call it as an event like below
$Button1.Add_click({Start-Runspace $SB})
Ok now back to that weird dispatcher stuff. When you want to make changes to things that are attached to different threads you need to use the synced hash table so you can get access to the variables, however you also need to use the dispatcher since the thread you are working in does not own the dialog, since you've already added the control to your hash table you just need to call it's dispatcher and let it know what action to perform, which you do in the scriptblock like I showed above.
$GLOBAL:sync.txtbox.Dispatcher.Invoke([action]{$GLOBAL:outputBox.Text = $wlanchk}, "Normal")
From there is just a matter of repeating for each of your buttons and actions. Hope this helps you get going
Edit
Just want to add that if you are interested in doing things a little more simply using XAML instead of defining your GUI using the drawing api's you should check out a small blog post I did Here, there's also some additional runspace stuff on that site but it's geared more towards multi-tasking than GUI usage.
EDIT
$Global:uiHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$Global:uiHash)
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ResizeMode="NoResize"
Title="Zapuskator" Height="350" Width="500">
<Grid>
<Button Name="button" Content="Button" HorizontalAlignment="Left" Margin="21,21,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button1" Content="Button1" HorizontalAlignment="Left" Margin="21,48,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button2" Content="Button2" HorizontalAlignment="Left" Margin="21,75,0,0" VerticalAlignment="Top" Width="200"/>
<Button Name="button3" Content="Button3" HorizontalAlignment="Left" Margin="21,102,0,0" VerticalAlignment="Top" Width="200"/>
<TextBox Name="textBox" HorizontalAlignment="Right" Height="151" Margin="0,159,60,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="422"/>
<GroupBox Name="groupBox" Header="Choose your session: " Margin="0,0,260,0" VerticalAlignment="Top" Height="138" HorizontalAlignment="Right" Width="222"/>
<Label Name="label" Content="60" HorizontalAlignment="Left" Margin="350,60,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.263,0.116" Height="60" Width="60"/>
</Grid>
</Window>
"#
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
$Global:uiHash.Button = $Global:uiHash.window.FindName("Button")
$Global:uiHash.Button1 = $Global:uiHash.window.FindName("Button1")
$Global:uiHash.Button2 = $Global:uiHash.window.FindName("Button2")
$Global:uiHash.Button3 = $Global:uiHash.window.FindName("Button3")
$Global:uiHash.TextBox = $Global:uiHash.window.FindName("textBox")
$Global:uiHash.groupBox = $Global:uiHash.window.FindName("groupBox")
$Global:uiHash.label = $Global:uiHash.window.FindName("label")
$Global:uiHash.Button.Add_click({$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("FFFFFFFF")}, "Normal")})
$Global:uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
Ok. Functions working as intended. I can change wallpapper or do whatever else. But I cannot pass text to textbox. Reworked UI with $GLOBAL:uiHash variable, it works, but... Like this I cannot change text on click. But when I try to change it like this:
Start-Sleep -s 5
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("FFFFF")},"Normal")
All fine, text appears.. What the..? What's wrong with it?
And by the way, Mike, text isnt changing, when I'm using your function. I think, I'm doing something wrong.