Responsive PowerShell WPF Form Introduction #2

Following on from my last post, I’m going to show you how to update a textbox using a button on the same form. I will be adding the following code starting on line 38:

#BUTTON LOGIC
$syncHash.Button.Add_Click({

    $syncHash.Window.Dispatcher.Invoke(
        [action]{
            $syncHash.TextBox.AppendText("This is a test")
        }
    )
})

This is fairly basic in what it does. It just adds “This is a test” to the textbox. Say if I want the button to run a task and then update the textbox with the results, but the results took a long time to come, the form would freeze. This is because whatever command you run in the same runspace as the GUI, takes controls and stops the GUI being responsive.

So, what I’m going to do is ping google 5 times, get the average from all of those and then update the textbox without the GUI becoming unresponsive. To do this, I’m going to create a new runspace and add the code I want to run. You can see this below:

#BUTTON LOGIC
$syncHash.Button.Add_Click({
    #ASSIGNING HOST VARIABLE
    $syncHash.host = $Host
    #CREATING NEW RUNSPACE
    $pingrunspace = [runspacefactory]::CreateRunspace()
    $pingrunspace.ApartmentState = "STA"
    $pingrunspace.ThreadOptions = "ReuseThread"
    $pingrunspace.Open()
    #PUTTING THE SYNCHASH VARIABLE INSIDE THE NEW RUNSPACE
    $pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

    #THIS IS THE CODE THAT WILL BE EXECUTED IN THE NEW RUNSPACE
    $code = {

        #CONNECTION TO GOOGLE AND CALCULATING AVERAGE IN NEW RUNSPACE
        $connection = Test-Connection -ComputerName google.co.uk -Count 5
        $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
        #UPDATING THE TEXTBOX WITH CONNECTION AVERAGE IN NEW RUNSPACE
        $syncHash.Window.Dispatcher.Invoke(
            [action]{
                $syncHash.TextBox.AppendText($average)
            }
        )

    }
        
    #ADDING AND RUNNING THE CODE IN THE NEW RUNSPACE
    $PSInstance = ::Create().AddScript($code)
    $PSinstance.Runspace = $pingrunspace
    $job = $PSinstance.BeginInvoke()
    
})

This will run the code in a separate runspace to the GUI and allow you to interact with it whilst the commands complete in the background.

Just in case you want the entire this, this is what the whole file looks like 🙂

#CREATE HASHTABLE AND RUNSPACE FOR GUI
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"         
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)      
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({   
    $xaml = @"
    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="Window" Height="400" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
        <TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
    </Grid>
</Window>
"@
  
    #INTERPRET AND LOAD THE GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #EXTRACT THE CONTROLS FROM THE GUI
    $syncHash.TextBox = $syncHash.window.FindName("Textbox")
    $syncHash.Button = $syncHash.Window.FindName("Button")

    #BUTTON LOGIC
    $syncHash.Button.Add_Click({

        $syncHash.host = $Host
        $pingrunspace = [runspacefactory]::CreateRunspace()
        $pingrunspace.ApartmentState = "STA"
        $pingrunspace.ThreadOptions = "ReuseThread"
        $pingrunspace.Open()
        $pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

        $code = {

            $connection = Test-Connection -ComputerName google.co.uk -Count 5
            $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
            $syncHash.Window.Dispatcher.Invoke(
                [action]{
                    $syncHash.TextBox.AppendText($average)
                }
            )

        }

        $PSInstance = ::Create().AddScript($code)
        $PSinstance.Runspace = $pingrunspace
        $job = $PSinstance.BeginInvoke()
    })


    #FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
    $Runspace.Close()
    $Runspace.Dispose()
    
})
#LOAD RUNSPACE WITH GUI IN
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Enjoy!

Responsive PowerShell WPF Form Introduction #1

Hooooly jebus chwist! This took a LONG time for me to get my head around and an even longer time to implement and get working (still breaking it every minute!). I used this website and this website to help me learn the basics.

Today, I’m going to show you how to create a responsive WPF from using PowerShell. This utilises runspaces and a synchronised hashta… never mind the technical stuff!

This is the code that I used:

#CREATE HASHTABLE AND RUNSPACE FOR GUI
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"         
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)      
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({   
    $xaml = @"
    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="Window" Height="400" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
        <TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
    </Grid>
</Window>
"@
  
    #INTERPRET AND LOAD THE GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #EXTRACT THE CONTROLS FROM THE GUI
    $syncHash.TextBox = $syncHash.window.FindName("Textbox")
    $syncHash.Button = $syncHash.Window.FindName("Button")

    #FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
    $Runspace.Close()
    $Runspace.Dispose()
    
})
#LOAD RUNSPACE WITH GUI IN
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Using this, you can then use the same command prompt used to launch the script to change the form. E.g. to change the text in the textbox we would use:

$syncHash.Window.Dispatcher.Invoke(
    [action]{$syncHash.TextBox.Text = "Updated text here"}
)

In another post, I’ll show you how to update the textbox using a button on the same form. Exciting stuff, right?

Leave a comment if you have any questions or issues. Enjoy!

Blackjack in PowerShell

This is a little function that mimics a simplified version of blackjack. I have wrapped it in a function for cleanliness and so that it can be called again.

The rules are below:

  • Get a higher total than the dealer
  • Keep your total under 21 or you will be bust
  • That’s it

Here is the code for you to try this out yourself, hope you enjoy!

function blackjack{

    #CHANGING NAME OF WINDOW
    $pshost = Get-Host
    $pswindow = $pshost.UI.RawUI
    $pswindow.WindowTitle = "Blackjack" 

    #RESETTING GAME OVER VARIABLE
    $blackjack_game_over = $false

    #GENERATING A RANDOM TOTAL FOR THE DEALER
    $blackjack_dealer_total = Get-Random -Minimum 14 -Maximum 22

    #CREATING AN ARRAY FOR THE USERS CARD NUMBERS
    $blackjack_user_card_array = [System.Collections.ArrayList]::new("")

    #GENERATING A RANDOM NUMBER FOR THE USERS FIRST CARD
    $blackjack_user_first_card = Get-Random -Minimum 1 -Maximum 11

    #ADDING USERS FIRST CARD TO ARRAY
    $blackjack_user_card_array.Add($blackjack_user_first_card)

    #CREATING A VARIABLE TO COUNT USERS TOTAL
    $blackjack_user_total = $blackjack_user_first_card

    Clear-Host

    Write-Host "Your first card is $blackjack_user_first_card"

    #DO THIS (PLAY GAME) UNTIL THE GAMEOVER VARIABLE IS TRUE
    do {
        #GET USER INPUT
        do {$blackjack_input = Read-Host "Take another card? (Y or N)"}while (("y","n") -notcontains $blackjack_input)

        #IF USER INPUT IS VALID AND ISN'T BUST AND WANTS ANOTHER CARD
        if ($blackjack_input -eq "y" -and $blackjack_user_total -le 21){

            #GENERATE A NEW CARD FOR THE USER
            $blackjack_user_new_card = Get-Random -Minimum 1 -Maximum 11

            #ADD NEW CARD TO CARD ARRAY
            $blackjack_user_card_array.Add($blackjack_user_new_card)

            #ADD NEW CARD TO CARD TOTAL
            $blackjack_user_total = $blackjack_user_total + $blackjack_user_new_card

            Clear-Host

            Write-Host "You have $blackjack_user_card_array"

            #IF THE USER IS BUST
            if ($blackjack_user_total -gt 21){
                Write-Host "You went bust! The dealer won with " -ForegroundColor Red -NoNewline
                Write-Host $blackjack_dealer_total 
                $blackjack_game_over = $true
            }
            
        #IF THE USER DOESNT WANT ANOTHER CARD
        }else{

            Clear-Host
            
            #OUTPUTTING THE FINAL SCORE
            #Write-Host "You had $blackjack_user_total and the dealer had $blackjack_dealer_total"

            #SWITCH TO SEE WHO WON
            switch ($blackjack_user_total){
                {$_ -gt 21}{Write-Host "You went bust! The dealer won with " -ForegroundColor Red -NoNewline; Write-Host $blackjack_dealer_total; $blackjack_game_over = $true; break}
                {$_ -eq $blackjack_dealer_total}{Write-Host "It's a draw, the dealer also had $blackjack_dealer_total"; $blackjack_game_over = $true; break}
                {$_ -gt $blackjack_dealer_total}{Write-Host "You win! The dealer only had " -ForegroundColor Green -NoNewline; Write-Host $blackjack_dealer_total; $blackjack_game_over = $true; break}
                {$_ -lt $blackjack_dealer_total}{Write-Host "You lose! The dealer won with " -ForegroundColor Red -NoNewline; Write-Host $blackjack_dealer_total; $blackjack_game_over = $true; break}
                default {Write-Host "Something happeneds that wasn't accounted for!" -ForegroundColor Red; break}
            }
        }
    }until ($blackjack_game_over)

    #ASK USER IF THEY WANT TO REPLAY UNTIL INPUT IS A Y OR N
    do {$blackjack_play_again = Read-Host "Do you want to play again? Y or N"} while (("y","n") -notcontains $blackjack_play_again)

    #SWITCH TO EITHER PLAY AGAIN OR GO TO MAIN MENU
    switch ($blackjack_play_again){
        "y" {blackjack}
        "n" {exit}
        default {exit}
    }   
}

 

Enumerating PowerShell Options

This is quite a handy trick that I use when designing or just fiddling around with what I can do with PowerShell WinForms. The last time I used this was to get all the possible colours I could use for my form background, and also check the possible options for my border on a panel.

So if I wanted to find all the possible colours available for my forms background, I would use the following:

[enum]::GetValues([System.ConsoleColor])

Black
DarkBlue
DarkGreen
DarkCyan
DarkRed
DarkMagenta
DarkYellow
Gray
DarkGray
Blue
Green
Cyan
Red
Magenta
Yellow
White

or if I wanted to find out all the possible borders for my panel as I stated above, I would use the following:

[enum]::GetValues([System.Windows.Forms.BorderStyle])

None
FixedSingle
Fixed3D

Hope this helps, I know this is usually helpful for me. Enjoy!

Server Reboot Script

Running a little low on content this last few months, plus I’ve been busy with other work stuff.

I had the requirement to create a PowerShell script that would get the uptime of a server and then decide whether or not the server needed rebooting.

I also wanted the script to randomize the reboot of the servers, that way if there are multiple servers that need rebooting at once, they don’t cause a power spike or resource issues on the hosts. I did this by creating a random number between 1 and 5 and then if the number equals 5, the server is rebooted. If not then the server isn’t rebooted.

This is the script that I ended up with and what is currently being tested:

$loglocation = "C:\scripts\reboot\log"
$dateforfile = Get-Date

#GETS UPTIME IN DAYS
$lastbootuptime = Get-WmiObject win32_operatingsystem
$uptime = (Get-Date) - ($lastbootuptime.converttodatetime($lastbootuptime.lastbootuptime))
$uptimeindays = $uptime.days

#GETS RANDOM NUMBER
$randomnumber = Get-Random -Minimum 1 -Maximum 6

if ($uptimeindays -ge "14"){

 Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at:
$dateforfile
This was an immediate shutdown as the server had been up for $uptimeindays days
"@

 Restart-Computer -Force

}elseif ($uptimeindays -lt "14" -and $uptimeindays -ge "7"){

    if ($randomnumber -eq "5"){

        Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at :
$dateforfile
This was a random restart as uptime was only $uptimeindays days
"@
        Restart-Computer -Force
    }else{

        Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server NOT restarted
$dateforfile
This was not randomly restarted. Uptime is currently $uptimeindays days. Random number was $randomnumber
"@
    }
}else{

Add-Content -Path "loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
No restart required
$dateforfile
No restart required since uptime is only $uptimeindays days
"@
}

The first time I created this script and set it up as a scheduled task, nothing happened. Turns out that I needed the -Force parameter in order for the server to be rebooted.

This will later be used in a group policy without the log creating as that is only necessary in the testing stage.

Enjoy!