0

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.

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for help. I have this GUI in WPF and XAML. Now I'm trying runspace method. I have no problems with adding simple functions to buttons, but add function in runspace... That was a problem :)
So did the method I posted to create the runspace work for you?
I think I need more help to figure it out.
OK no problem, let me get settled at work for a bit and I'll post some additional info. Can you show me whatever you've tried to put together so far?
Sure. See answer above. Now with XAML :)
0

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.

11 Comments

Check out my edit, if you have trouble with it remove the timer while you get the rest of it set up and then re-add it, you'll likely need to wrap it in it's own runspace separate from the dialog.
Could you check my edit please and say, what's wrong now? I didnt test much my WPF script, was a really hard day. But text in textbox doesnt want to change. Spent my time to write clean GUI in XAML and test it.
Try using the Textbox's dispatcher instead of the windows.
Tried. Still no result. Don't know what's wrong with it.
Ok found the issue, WPF is case-sensitive, in your XAML you named but the button "button" but when you do your find names you use "Button", just make sure all your cases match and it will work fine(This is a good habit to get into anyway, PS is one of the rare langs that are not case-sensitive), just tested on my end and was able to get the textbox to update after swapping the case.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.