Multiple functions in WPF GUI from runspace(s) - wpf

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.

Related

Powershell GUI not registering Radio Button as clicked [duplicate]

This question already has answers here:
WPF if statement based on radio button checked or not
(3 answers)
Closed 1 year ago.
I have the following code:
$var_executeButton.Add_Click( {
if ($var_radioButton.Checked) {
..do stuff
}
})
however, the code inside the if block never run, even if radio button is checked.
Code outside of the if works just fine.
Can you please tell me why?
Continuing from my comment. There ways to take action on form elements at the time they are acted on or checked later. As an example here is a simple WinForm, that discovers all radio button controls on a form and takes the same action when any one of them is clicked.
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$objForm = New-Object System.Windows.Forms.Form
$objForm.Text = "Test"
$objForm.Size = New-Object System.Drawing.Size(400, 200)
$objForm.StartPosition = "CenterScreen"
$RadioButton1 = New-Object System.Windows.Forms.RadioButton
$RadioButton1.Location = New-Object System.Drawing.Size(10, 10)
$RadioButton1.Size = New-Object System.Drawing.Size(100, 20)
$RadioButton1.Checked = $false
$RadioButton1.text = "Button1"
$objForm.controls.Add($RadioButton1)
$RadioButton2 = New-Object System.Windows.Forms.RadioButton
$RadioButton2.Location = New-Object System.Drawing.Size(10, 50)
$RadioButton2.Size = New-Object System.Drawing.Size(100, 20)
$RadioButton2.Checked = $false
$RadioButton2.text = "Button2"
$objForm.controls.Add($RadioButton2)
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(10, 120)
$OKButton.Size = New-Object System.Drawing.Size(150, 30)
$OKButton.Text = "OK"
$OKButton.Enabled = $false
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$objForm.Controls.Add($OKButton)
$objForm.AcceptButton = $OKButton
$objForm.TopMost = $True
$objForm.Controls |
Where-Object { $PSItem -is [System.Windows.Forms.RadioButton] } |
ForEach-Object {
$PSItem.Add_Click(
{
If (-Not $OKButton.Enabled)
{$OKButton.Enabled = $True}
})
}
$Form = $objForm.ShowDialog()
To see if a radio button is checked, you can use either .Checked,
# Example - check radio button state on OK click.
function Test-RadioButtonCheck
{
Param
(
$title,
$a1,
$a2,
$a3
)
$form = New-Object System.Windows.Forms.Form
$form.Size = '300,250'
$form.Text = $title
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$label1 = New-Object System.Windows.Forms.Label
$label1.Location = '15,15'
$label1.Width = 100
$label1.Text = 'Enter some text'
$form.Controls.Add($label1)
$textBox1 = New-Object System.Windows.Forms.TextBox
$textBox1.Location = '115,15'
$textBox1.width = 150
$textBox1.Name = 'input'
$form.Controls.Add($textBox1)
$radio1 = New-Object System.Windows.Forms.RadioButton
$radio1.Location = '25,40'
$radio1.Text = 'Check me if you want the save'
$radio1.Width = 200
$form.Controls.Add($radio1)
$button = New-Object System.Windows.Forms.Button
$button.Location = '70,70'
$button.Text = 'Ok'
$button.DialogResult = 'Ok'
$form.Controls.Add($button)
if($form.ShowDialog() -eq 'Ok')
{
if($textBox1.Text)
{
if($radio1.Checked){$form}
else{[void][System.Windows.Forms.MessageBox]::Show('You didn''t check the radio button','Error')}
}
else{[void][System.Windows.Forms.MessageBox]::Show('You didn''t enter any text','Error')}
}
}
if($frm = Test-RadioButtonCheck 'Test form')
{Write-Host "You entered $($frm.Controls['input'].Text)" -ForegroundColor Green}
.IsChecked as per mklement0's link, or looping to find them:
Where {$PSItem -is [system.windows.controls.radiobutton] -and $PSItem.IsChecked} |
Select Name
... or using [System.Windows.Controls.RadioButton]::CheckedEvent as per this WPF example:
[xml]$xaml = #"
<Window
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
x:Name = "Window" Title = "Initial Window" WindowStartupLocation = "CenterScreen"
SizeToContent = "WidthAndHeight" ShowInTaskbar = "True" Background = "lightgray">
<StackPanel x:Name = 'StackPanel'>
<RadioButton x:Name = "Item1" Content = 'Item1'/>
<RadioButton x:Name = "Item2" Content = 'Item2'/>
<RadioButton x:Name = "Item3" Content = 'Item3'/>
<Separator/>
<TextBox x:Name = 'textbox'/>
</StackPanel>
</Window>
"#
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$Window = [Windows.Markup.XamlReader]::Load( $reader )
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") |
ForEach {Set-Variable -Name ($PSItem.Name) -Value $Window.FindName($PSItem.Name) -Scope Script}
#Bubble up event handler
[System.Windows.RoutedEventHandler]$Script:CheckedEventHandler =
{$TextBox.Text = $PSItem.source.name}
$StackPanel.AddHandler([System.Windows.Controls.RadioButton]::
CheckedEvent, $CheckedEventHandler)
$Window.Showdialog() | Out-Null
If you want dig at the explanations of the WPF and radio button options, then see this blog post regarding the samples shown:
https://learn-powershell.net/2014/08/10/powershell-and-wpf-radio-button
If the aforementioned is not what you are doing, then as per my comment, you are not showing what the use case really is.

Blinking text in a form creating by PowerShell

I search the way to blink a text label in a windows.form.
This is a part of my script :
$Form = New-Object system.Windows.Forms.Form
$Form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$Form.ClientSize = '600,800'
$Form.text = "USMT - Sauvegarde des profils"
$Label1 = New-Object system.Windows.Forms.Label
$Label1.text = "1. Chemin vers scanstate.exe :"
$Label1.AutoSize = $true
$Label1.width = 25
$Label1.height = 10
$Label1.location = New-Object System.Drawing.Point(20,10)
$Label1.Font = 'Microsoft Sans Serif,10'
Here I search a solution to blink $Label1.text
Thanks for help !
Create a timer, add an event handler to its Tick event and toggle visibility of the label from there. Creating GUIs and reacting to events isn't particularly nice in PowerShell, but it can be done.
You can create a System.Windows.Forms.Timer object and set its Interval to a suitable value (milliseconds) and then by handling its Tick event, do whatever you need to blink the label, for example toggle visibility of the label. You need to start the timer when loading form:
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$label = New-Object System.Windows.Forms.Label
$label.Text = "This is my label."
$label.AutoSize = $true
$form.Controls.Add($label)
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 250
$timer.Add_Tick({$label.Visible = -not($label.Visible)})
$form.Add_Load({$timer.Start()})
$form.ShowDialog()
$timer.Dispose()
$form.Dispose()

powershell combobox items to variable

I am trying to create a powershell Script with a Gui that gives the user a drop list of available drives and then when the user selects the drive and clicks ok the script maps that drive as M:\
But I cant work out how to get the selected item in the combo box to be passed into the variable $MapDrive
#List of Drives To Map
$DriveList = #("0. DGL_Data","1. P1","2. DGLFSG3","3. p3 (TPC)","4. p4","6. p6","7. p7","8. p8","9. p9","10. p10","11. p11","12. p12")
#Displays With Drive to Map
$MapDrive = convert.ToString($Dropdown.SelectedItem)
Function MapDrive {
If ($MapDrive -eq $DriveList[0]){
Write-Output Sucess > "C:\Users\andy.burton\Desktop\Practice Selector\Success.txt"}
ElseIf ($MapDrive -eq $DriveList[1]){
Write-Output BooYah > "C:\Users\andy.burton\Desktop\Practice Selector\Yes.txt"
}
Else {
Write-Output Failure > "C:\Users\andy.burton\Desktop\Practice Selector\Failed.txt"}
}
#test Function
Function test {
Write-Output $MapDrive > "C:\Users\andy.burton\Desktop\Practice Selector\Success.txt"
}
#Function to Create Form
Function GenerateForm {
#Define Drive Selector Main Form
Add-Type -AssemblyName System.Windows.Forms
$DGL = New-Object system.Windows.Forms.Form
$DGL.Text = "DGL Practice Manager"
$DGL.TopMost = $true
$DGL.BackgroundImage = [system.drawing.image]::FromFile("C:\Users\andy.burton\Desktop\Practice Selector\Images\medical.jpg")
$DGL.Icon = New-Object system.drawing.icon("C:\Users\andy.burton\Desktop\Practice Selector\Images\medical2.ico")
$DGL.Width = 600
$DGL.Height = 265
$DGL.MinimizeBox = $False
$DGL.MaximizeBox = $False
#Label to Display Instuctions
$label2 = New-Object system.windows.Forms.Label
$label2.Text = "Select which drive"
$label2.BackColor = "#e4f3fa"
$label2.AutoSize = $true
$label2.Width = 25
$label2.Height = 10
$label2.location = new-object system.drawing.point(20,28)
$label2.Font = "Microsoft Sans Serif,10"
$DGL.controls.Add($label2)
#Dropdown Box For Selecting Practice
$Dropdown = New-Object system.windows.Forms.ComboBox
$Dropdown.BackColor = "#e4f3fa"
$DropDown.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
$Dropdown.Width = 243
$Dropdown.Height = 20
$Dropdown.location = new-object system.drawing.point(21,73)
$Dropdown.Font = "Microsoft Sans Serif,10"
$DropDown.items.addrange($DriveList)
$DGL.controls.Add($Dropdown)
#Cancel Button to Cancel drive Selection
$Cancelbutton = New-Object system.windows.Forms.Button
$Cancelbutton.Text = "Cancel"
$Cancelbutton.Width = 60
$Cancelbutton.Height = 30
$Cancelbutton.location = new-object system.drawing.point(210,120)
$Cancelbutton.Font = "Microsoft Sans Serif,10"
$DGL.CancelButton = $Cancelbutton
$CancelButton.Add_Click({ $DGL.close();[System.Windows.Forms.Application]::Exit($null)})
$DGL.controls.Add($Cancelbutton)
#OK Button to Select Drive
$OKbutton = New-Object system.windows.Forms.Button
$OKbutton.Text = "OK"
$OKbutton.Width = 60
$OKbutton.Height = 30
$OKbutton.location = new-object system.drawing.point(140,120)
$OKbutton.Font = "Microsoft Sans Serif,10"
$DGL.AcceptButton = $OKbutton
$MapDrive = convert.ToString($Dropdown.SelectedItem)
#On click call PracticeSelectedCallBack to launch the application
$OKbutton.Add_Click({test ; $DGL.close()})
$DGL.controls.Add($OKbutton)
#Display the Form
$DGL.Add_Shown({$DGL.Activate()})
$DGL.ShowDialog()
}
GenerateForm
I also want to hide the powershell window but not the gui I have tried -window hidden but that hid everything
The ComboBox has several events that you can tie into that will do various things. One of the events is a SelectedIndexChanged. You can add that event to your ComboBox object and update $MapDrive
This code $MapDrive = convert.ToString($Dropdown.SelectedItem) will only fire once during the initial compile. You have to use events to trigger code changes after compile during runtime.
Also, in Powershell you can use the following command [System.Windows.Forms.ComboBox] | gm to get a list of the methods, and properties of the object. You can use [System.Windows.Forms.checkedlistbox].GetEvents() to get a list of events of an object.

Powershell $button.add_click executes the whole code inside the add_click block

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

Display custom message while installation using powershell

i am unable to figure out the mistake that i am doing.The following script will call a bunch of batch files and while the batch files are doing there job, the progress will be shown in a progress bar along with the name of the script. What i want to achieve is to display a custom message during the installation process. I am not able to find out the mistake, any help is deeply appreciated.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
Set-Location $PSScriptRoot
#Call scripts for installation
$ScriptsHome = Get-Item '.\test\forPS\*'
#Define Form
# Init Form
$Form = New-Object System.Windows.Forms.Form
$Form.width = 1000
$Form.height = 200
$Form.Text = "** Installation in Progress-PLEASE DO NOT CLOSE THIS WINDOW**"
$Form.Font = New-Object System.Drawing.Font("Times New Roman" ,12, [System.Drawing.FontStyle]::Regular)
$Form.MinimizeBox = $False
$Form.MaximizeBox = $False
$Form.WindowState = "Normal"
$Form.StartPosition = "CenterScreen"
$Form.Opacity = .8
$Form.BackColor = "Gray"
#Define ICON for Form
$Icon = New-Object System.Drawing.Graphics (".\ICON.jpg")
$Form.Icon = $Icon
# Init ProgressBar
$ProgressBar = New-Object System.Windows.Forms.ProgressBar
$ProgressBar.Maximum = $ScriptsHome.Count
$ProgressBar.Minimum = 0
$ProgressBar.Location = new-object System.Drawing.Size(10,70)
$ProgressBar.size = new-object System.Drawing.Size(967,10)
$Form.Controls.Add($ProgressBar)
$Form.Controls.Add($Messages)
#Running Script Name
$Label = New-Object System.Windows.Forms.Label
$Label.AutoSize = $true
$Label.Location = New-Object System.Drawing.Point(10,50)
$Form.Controls.Add($Label)
#Define Array messages
#Array
$Messages = #("Preparing to install patch set..Preparing to stop all related processes",
"Upgrading the application",
"Copying the missing folder",
"Applying the patch",
"Starting all previously stopped Services",
"Checkcing healthyness of the system after the installation this is may take up to half an hour...",
"Rebooting Server"
)
$Messages = New-Object System.Windows.Forms.Label
$Messages.AutoSize = $true
$Messages.Location = New-Object System.Drawing.Point(10,50)
$Form.Controls.Add($Messages)
# Add_Shown action
$ShownFormAction = {
$Form.Activate()
foreach ($script in $ScriptsHome) {
$ProgressBar.Increment(1)
#$Messages.Text = $Messages[$Messages]
$Label.Text = "$($script.Name)"
Start-Process $script.FullName -Wait -WindowStyle Hidden
}
$Form.Dispose()
}
$Form.Add_Shown($ShownFormAction)
# Show Form
$Form.ShowDialog()
Thanks in advance.
You're reusing the same variable name for the list of messages and the label showing the message itself:
$Messages = #("Preparing to install patch set..Preparing to stop all related processes",
"Upgrading the application",
"Copying the missing folder",
"Applying the patch",
"Starting all previously stopped Services",
"Checkcing healthyness of the system after the installation this is may take up to half an hour...",
"Rebooting Server"
)
$Messages = New-Object System.Windows.Forms.Label
$Messages.AutoSize = $true
$Messages.Location = New-Object System.Drawing.Point(10,50)
Rename one of them (ie. use $MessageLabel for the label):
$MessageLabel = New-Object System.Windows.Forms.Label
$MessageLabel.AutoSize = $true
$MessageLabel.Location = New-Object System.Drawing.Point(10,50)
Since you increment the ProgressBar by 1 every step, you can reuse the progress bar value to index into the $Messages array:
foreach ($script in $ScriptsHome) {
$ProgressBar.Increment(1)
$MessageLabel.Text = $Messages[$ProgressBar.Value - 1]
$Label.Text = "$($script.Name)"
Start-Process $script.FullName -Wait -WindowStyle Hidden
}

Resources