Part II – Deploying PowerShell GUIs in Minutes using Visual Studio

series_PowerShellGUI

This post is part of the Learning GUI Toolmaking Series, here on FoxDeploy. Click the banner to return to the series jump page!


I got a lot of feedback last time, everyone wants the rest of the series, and you guys want it now! So I’m skipping my normal 1,000 word limit for this post and making this bad-boy LONG! There will still be a part three where I’ll show you how to use some of the trickier form elements. Some of them are absolute hacks to make them work in PowerShell, so if you’re the god of XAML and WPF, please have mercy on us mere-mortals and add some comments to let us know how I ought to be doing it.

Let’s jump back in and take our finished XAMl from last time and put it into PowerShell.

Whoa whoa, what’s XAML

I don’t know if you noticed this window. This whole time we’ve been adding elements, dropping boxes and text and things like that, it’s been updating in real time!

ZAMMLE_pic0

The language here is XAML, (Extensible Application Markup Language) which is a Microsoft language built on XML to make very small but effective applications easily. We’ll copy this code, but first, we need to make some changes to it before it will ‘just work’.

If you want to play around with my example, just copy this stuff

<Window x:Class="WpfApplication2.MainWindow"
        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:WpfApplication2"
        mc:Ignorable="d"
        Title="FoxDeploy Awesome Tool" Height="345.992" Width="530.344" Topmost="True">
    <Grid Margin="0,0,45,0">
        <Image x:Name="image" HorizontalAlignment="Left" Height="100" Margin="24,28,0,0" VerticalAlignment="Top" Width="100" Source="C:\Users\Stephen\Dropbox\Docs\blog\foxdeploy favicon.png"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Height="100" Margin="174,28,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="282" FontSize="16"><Run Text="Use this tool to find out all sorts of useful disk information, and also to get rich input from your scripts and tools"/><InlineUIContainer>
        		<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="TextBlock"/>
        	</InlineUIContainer></TextBlock>
        <Button x:Name="button" Content="OK" HorizontalAlignment="Left" Height="55" Margin="370,235,0,0" VerticalAlignment="Top" Width="102" FontSize="18.667"/>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="35" Margin="221,166,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="168" FontSize="16"/>
        <Label x:Name="label" Content="UserName" HorizontalAlignment="Left" Height="46" Margin="56,162,0,0" VerticalAlignment="Top" Width="138" FontSize="16"/>

    </Grid>
</Window>

Basically, we need to remove some properties from the window declaration, and also remove the x: before each name. But don’t do all of that by hand, just copy and paste it into this little blob I’ve made for you 🙂

Importing this into PowerShell

#ERASE ALL THIS AND PUT XAML BELOW between the @" "@
$inputXML = @"
<Window x:Class="Azure.Window1"
        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:Azure"
        mc:Ignorable="d"

        Title="iVision Azure Accelerator" Height="524.256" Width="332.076">
    <Grid Margin="0,0,174,0">
    </Grid>
</Window>
"@        

$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'

[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{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}

#===========================================================================
# Load XAML Objects In PowerShell
#===========================================================================

$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name)}

Function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}

Get-FormVariables

#===========================================================================
# Actually make the objects work
#===========================================================================

#Sample entry of how to add data to a field

#$vmpicklistView.items.Add([pscustomobject]@{'VMName'=($_).Name;Status=$_.Status;Other="Yes"})

#===========================================================================
# Shows the form
#===========================================================================
write-host "To show the form, run the following" -ForegroundColor Cyan
'$Form.ShowDialog() | out-null'

If you’ve taken a look at the blog article on the MS site which I linked last week, some of the above might look very familiar. In fact, I’ve added a bit of extra output to help us in troubleshooting (by dumping a list of all of the variables relating to objects on our form) and made this into a snippet, which you can download and embed in your own ISE.

That’s a lot of code, but you only need to copy and paste your XAML from VisualStudio to PowerShell in between the here-string.

What’s a here-string?

If you’ve never heard of a here-string, it’s a programming term for a multi-line variable that maintains spacing and allows for variable expansion. Long-story short, it’s the @” “@ above.

When you’ve copied and pasted your XAML into the here-string above and hit F5 in the PowerShell ISE (or wherever you edit scripts), you’ll see the following:

Running Convert tool

Our tool has scanned through the GUI and created hooks associated with every interactable element on the screen. We can now make changes to these things with code, just by changing their objects. This little display here is actually a function embedded in the code. You can run it again later if you forget the variable names by running Get-FormVariables.

As you see above, if we want to run the GUI, we just type in

>$Form.ShowDialog() | Out-Null

That was SO EASY!
That was SO EASY!

Changing values on the GUI is easy!

Now, what if we wanted to do something a bit more complex, like change the value of the Text where it says ‘TextBox’.

We need to hook into the properties that the tool displayed to us earlier. In this case, the name is $WPFTextBox. This is an object which this refers to the object on our form. That means we can change the text just by looking for a .text property on this object.

$WPFtextBox.Text
>TextBox

If we change this with a simple equals statement…

$WPFtextbox.Text = 'Hello World'

Hello World

This is the basic flow we’ll take to interact with all of our GUIs hence-forth. Draw something cool in Visual Studio, copy and paste it into the snippet, then run Get-FormVariables and see what the name is for our new cool GUI features (they’re called ‘Form controls’ if we want to be specific). Then look at the new object and see what it’s properties and methods are.

But it doesn’t work…

One last thing before we fully upgrade this into an awesome tool, let’s try clicking the OK Button.

Nothing happens! Here’s why: by default, we need to give our button an action to run when we click it. You do this using the Add_Click() method, which lets us put a {script-block} into the () overload, which will be executed when the user clicks a button. This makes our buttons a great place to setup hooks if we want to grab the values from a box.

For instance, if we want our OK button to just close our form, we run this little number

$WPFbutton.Add_Click({$form.Close()})

After the form has closed, the value of all of these objects in the form still persist, so if the user made typed something like ‘YoDawg’ into our textbox, we can get it once the form is gone by running:

$WPFtextBox.Text

YoDawg

Alright, let’s make this into a WMI info gathering tool.

Building a better WMI tool

I’ve got a tool that I walk people through making in my Learning PowerShell bootcamp course (if you want me to come deliver one at your company, send me a message!), in which we learn how to query WMI/CIM and then expand from there and make it do cool fancy stuffs. The output of the tool at the end of the day looks like this.
Driveinfo

We can make a GUI version of this by adding a ListView, which is pretty much embedding something like an Excel datasheet into this GUI. To do this, click ListView in the ToolBox and drag it to your form and size appropriately. You’ll notice that I also made a few tweaks to the layout to better fit what we want this tool to do.

Layout pregridview

You can just barely make it out, but there is a grid there now, waiting to receive our beautiful rows and columns. Let’s add some stuff, just a warning, this can be a bit tricky at first.

In Visual Studio, in the XAML, click the GridView Tag.

Layout Click Gridview

In properties on the right, click ‘Columns’

Layout AddColumns
This will bring up the Column collection editor

Column editor

Now, in this area, you’ll want to click the ‘Add’ button and change the width to about 100 for each, and specify the column name in the Header Box. I’ll add one each for each of the fields my tool returns:

• Drive Letter
• Drive Label
• Size(MB)
• FreeSpace%

As before, you can change the font by clicking on the area with text, then go to Properties>Text on the right side of the screen. When finished you should have something like this:

Columns

If we want to make our buttons and form actually work though, we’ll need to hook into the form again, as we did previously.

Making all of the new stuff work

If you want to catch up with where we are now in the walkthrough, get this stuff:

#ERASE ALL THIS AND PUT XAML BELOW between the @" "@
$inputXML = @"
<Window x:Class="WpfApplication2.MainWindow"
        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:WpfApplication2"
        mc:Ignorable="d"
        Title="FoxDeploy Awesome Tool" Height="416.794" Width="598.474" Topmost="True">
    <Grid Margin="0,0,45,0">
        <Image x:Name="image" HorizontalAlignment="Left" Height="100" Margin="24,28,0,0" VerticalAlignment="Top" Width="100" Source="C:\Users\Stephen\Dropbox\Docs\blog\foxdeploy favicon.png"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Height="100" Margin="174,28,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="282" FontSize="16"><Run Text="Use this tool to find out all sorts of useful disk information, and also to get rich input from your scripts and tools"/><InlineUIContainer>
        		<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="TextBlock"/>
        	</InlineUIContainer></TextBlock>
        <Button x:Name="button" Content="get-DiskInfo" HorizontalAlignment="Left" Height="35" Margin="393,144,0,0" VerticalAlignment="Top" Width="121" FontSize="18.667"/>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="35" Margin="186,144,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="168" FontSize="16"/>
        <Label x:Name="label" Content="ComputerName" HorizontalAlignment="Left" Height="46" Margin="24,144,0,0" VerticalAlignment="Top" Width="138" FontSize="16"/>
        <ListView x:Name="listView" HorizontalAlignment="Left" Height="156" Margin="24,195,0,0" VerticalAlignment="Top" Width="511" FontSize="16">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Drive Letter" Width="120"/>
                    <GridViewColumn Header="Drive Label" Width="120"/>
                    <GridViewColumn Header="Size(MB)" Width="120"/>
                    <GridViewColumn Header="FreeSpace%" Width="120"/>
                </GridView>
            </ListView.View>
        </ListView>

    </Grid>
</Window>

"@        

$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'

[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{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}

#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================

$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name)}

Function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}

Get-FormVariables

#===========================================================================
# Actually make the objects work
#===========================================================================

$WPFbutton.Add_Click({
$userInput = $WPFtextBox.Text
$form.Close()
})
#Sample entry of how to add data to a field

#$vmpicklistView.items.Add([pscustomobject]@{'VMName'=($_).Name;Status=$_.Status;Other="Yes"})

#===========================================================================
# Shows the form
#===========================================================================
write-host "To show the form, run the following" -ForegroundColor Cyan
'$Form.ShowDialog() | out-null'

If you scroll to the bottom, just below Get-FormVariables, you’ll see an example of how to add data to a field. This part of our script is where the XAML has been parsed, and objects have been created to hook into them. This is where we’ll need to put our magic sauce to make the buttons and fields work and do cool things.

Where-To-make-changes

So, scroll down to the ‘Make the Objects Actually Work’ area.

First things first, take this snippet of code which accepts a computer name and returns the disk information:

Function Get-DiskInfo {
param($computername =$env:COMPUTERNAME)

Get-WMIObject Win32_logicaldisk -ComputerName $computername | Select-Object @{Name='ComputerName';Ex={$computername}},`
                                                                    @{Name=‘Drive Letter‘;Expression={$_.DeviceID}},`
                                                                    @{Name=‘Drive Label’;Expression={$_.VolumeName}},`
                                                                    @{Name=‘Size(MB)’;Expression={[int]($_.Size / 1MB)}},`
                                                                    @{Name=‘FreeSpace%’;Expression={[math]::Round($_.FreeSpace / $_.Size,2)*100}}
                                                                 }

Here is what to do next.

  • I want my textbox to default to displaying my computer name
  • I want to add a trigger that when I click the Get-DiskInfo button, it should run the Get-DiskInfo function, using the computer name specified in the textbox
  • Finally, I want to take the objects I get from that and for each of them, add a new row to my ListView area

Change your code to the following, beginning on line 61 (or just below Get-FormVariables)

Function Get-DiskInfo {
param($computername =$env:COMPUTERNAME)

Get-WMIObject Win32_logicaldisk -ComputerName $computername | Select-Object @{Name='ComputerName';Ex={$computername}},`
                                                                    @{Name=‘Drive Letter‘;Expression={$_.DeviceID}},`
                                                                    @{Name=‘Drive Label’;Expression={$_.VolumeName}},`
                                                                    @{Name=‘Size(MB)’;Expression={[int]($_.Size / 1MB)}},`
                                                                    @{Name=‘FreeSpace%’;Expression={[math]::Round($_.FreeSpace / $_.Size,2)*100}}
                                                                 }

$WPFtextBox.Text = $env:COMPUTERNAME

$WPFbutton.Add_Click({
Get-DiskInfo -computername $WPFtextBox.Text | % {$WPFlistView.AddChild($_)}
})

Let’s run it and see what happen’s when you click the button.

If it breaks in a new way, I call that progress

Layout almost there

Well, crap. Adding new rows worked, but now every column has the output for every property. This is happening because, very similar to when you work with the pipeline in PowerShell or make a function, you have to tell PowerShell how to bind to values.

To fix this, go up to your XAML for your GridView Columns and add a DisplayMemberBinding Property like this. Make sure if you’re deviating from the walkthrough and doing your own thing to pick names that make sense. If your name has a space in it, use single quotes around it.

<GridView>
                    <GridViewColumn Header="Drive Letter" DisplayMemberBinding ="{Binding 'Drive Letter'}" Width="120"/>
                    <GridViewColumn Header="Drive Label" DisplayMemberBinding ="{Binding 'Drive Label'}" Width="120"/>
                    <GridViewColumn Header="Size(MB)" DisplayMemberBinding ="{Binding Size(MB)}" Width="120"/>
                    <GridViewColumn Header="FreeSpace%" DisplayMemberBinding ="{Binding FreeSpace%}" Width="120"/>
                </GridView>

And the finished product:

done

Whats up next?

Alright guys, I want to thank you for sticking with me to the end of this VERY long blog post. I hope you enjoy it and will use this technique to make some awesome GUIs of your own.

Join me for my post next time on this topic, part III in the GUI series, in which we dig into how to add some of the cooler and more difficult features to our GUI, like a tabbed interface (to get other WMI values) and how to use checkboxes and radio buttons, dropdown boxes and more!

Part III – Using Advanced GUI Elements in PowerShell

Final XAML Code here

<Window x:Class="WpfApplication2.MainWindow"
        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:WpfApplication2"
        mc:Ignorable="d"
        Title="FoxDeploy Awesome Tool" Height="416.794" Width="598.474" Topmost="True">
    <Grid Margin="0,0,45,0">
        <Image x:Name="image" HorizontalAlignment="Left" Height="100" Margin="24,28,0,0" VerticalAlignment="Top" Width="100" Source="C:\Users\Stephen\Dropbox\Docs\blog\foxdeploy favicon.png"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Height="100" Margin="174,28,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="282" FontSize="16"><Run Text="Use this tool to find out all sorts of useful disk information, and also to get rich input from your scripts and tools"/><InlineUIContainer>
        		<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="TextBlock"/>
        	</InlineUIContainer></TextBlock>
        <Button x:Name="button" Content="get-DiskInfo" HorizontalAlignment="Left" Height="35" Margin="393,144,0,0" VerticalAlignment="Top" Width="121" FontSize="18.667"/>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="35" Margin="186,144,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="168" FontSize="16"/>
        <Label x:Name="label" Content="ComputerName" HorizontalAlignment="Left" Height="46" Margin="24,144,0,0" VerticalAlignment="Top" Width="138" FontSize="16"/>
        <ListView x:Name="listView" HorizontalAlignment="Left" Height="156" Margin="24,195,0,0" VerticalAlignment="Top" Width="511" FontSize="16">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Drive Letter" DisplayMemberBinding ="{Binding 'Drive Letter'}" Width="120"/>
                    <GridViewColumn Header="Drive Label" DisplayMemberBinding ="{Binding 'Drive Label'}" Width="120"/>
                    <GridViewColumn Header="Size(MB)" DisplayMemberBinding ="{Binding Size(MB)}" Width="120"/>
                    <GridViewColumn Header="FreeSpace%" DisplayMemberBinding ="{Binding FreeSpace%}" Width="120"/>
                </GridView>
            </ListView.View>
        </ListView>

    </Grid>
</Window>

Full PowerShell Code here

#ERASE ALL THIS AND PUT XAML BELOW between the @" "@
$inputXML = @"
<Window x:Class="WpfApplication2.MainWindow"
        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:WpfApplication2"
        mc:Ignorable="d"
        Title="FoxDeploy Awesome Tool" Height="416.794" Width="598.474" Topmost="True">
    <Grid Margin="0,0,45,0">
        <Image x:Name="image" HorizontalAlignment="Left" Height="100" Margin="24,28,0,0" VerticalAlignment="Top" Width="100" Source="C:\Users\Stephen\Dropbox\Docs\blog\foxdeploy favicon.png"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Height="100" Margin="174,28,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="282" FontSize="16"><Run Text="Use this tool to find out all sorts of useful disk information, and also to get rich input from your scripts and tools"/><InlineUIContainer>
        		<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="TextBlock"/>
        	</InlineUIContainer></TextBlock>
        <Button x:Name="button" Content="get-DiskInfo" HorizontalAlignment="Left" Height="35" Margin="393,144,0,0" VerticalAlignment="Top" Width="121" FontSize="18.667"/>
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="35" Margin="186,144,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="168" FontSize="16"/>
        <Label x:Name="label" Content="ComputerName" HorizontalAlignment="Left" Height="46" Margin="24,144,0,0" VerticalAlignment="Top" Width="138" FontSize="16"/>
        <ListView x:Name="listView" HorizontalAlignment="Left" Height="156" Margin="24,195,0,0" VerticalAlignment="Top" Width="511" FontSize="16">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Drive Letter" DisplayMemberBinding ="{Binding 'Drive Letter'}" Width="120"/>
                    <GridViewColumn Header="Drive Label" DisplayMemberBinding ="{Binding 'Drive Label'}" Width="120"/>
                    <GridViewColumn Header="Size(MB)" DisplayMemberBinding ="{Binding Size(MB)}" Width="120"/>
                    <GridViewColumn Header="FreeSpace%" DisplayMemberBinding ="{Binding FreeSpace%}" Width="120"/>
                </GridView>
            </ListView.View>
        </ListView>

    </Grid>
</Window>

"@        

$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N'  -replace '^<Win.*', '<Window'

[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{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}

#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================

$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name)}

Function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}

Get-FormVariables

#===========================================================================
# Actually make the objects work
#===========================================================================

Function Get-DiskInfo {
param($computername =$env:COMPUTERNAME)

Get-WMIObject Win32_logicaldisk -ComputerName $computername | Select-Object @{Name='ComputerName';Ex={$computername}},`
                                                                    @{Name=‘Drive Letter‘;Expression={$_.DeviceID}},`
                                                                    @{Name=‘Drive Label’;Expression={$_.VolumeName}},`
                                                                    @{Name=‘Size(MB)’;Expression={[int]($_.Size / 1MB)}},`
                                                                    @{Name=‘FreeSpace%’;Expression={[math]::Round($_.FreeSpace / $_.Size,2)*100}}
                                                                 }

$WPFtextBox.Text = $env:COMPUTERNAME

$WPFbutton.Add_Click({
$WPFlistView.Items.Clear()
start-sleep -Milliseconds 840
Get-DiskInfo -computername $WPFtextBox.Text | % {$WPFlistView.AddChild($_)}
})
#Sample entry of how to add data to a field

#$vmpicklistView.items.Add([pscustomobject]@{'VMName'=($_).Name;Status=$_.Status;Other="Yes"})

#===========================================================================
# Shows the form
#===========================================================================
write-host "To show the form, run the following" -ForegroundColor Cyan
$Form.ShowDialog() | out-null

Link to the ISE Snippet

165 thoughts on “Part II – Deploying PowerShell GUIs in Minutes using Visual Studio

  1. rainer-michael kennedy October 24, 2016 / 6:40 pm

    Absolutely amazing guide. I am new to adding GUI’s to Powershell so i’m not so good at the moment!🙂

    I have completed the script however the Get-Variable WPF* is not finding anything, and when i run the script without the ‘ ‘ between the show dialog at the bottom, I just get a blank iVision Azure Accelerator pop-up.

    My code can be found here – http://pastebin.com/m4PauNm0

    Many Thanks!

    • FoxDeploy October 24, 2016 / 8:19 pm

      LOL, your first line says $inportXML, instead of $inputXML. 🙂 Once I changed that, I got some .net errors. These happen because PowerShell doesn’t support both the Click and TextChanged properties within XAML. You can do those (and I cover how to do them in later posts) but just not within XAML, so I removed them all. And it loads.

      Looks pretty good to, very nice tool. Show this to your boss and demand a raise. The IT Best Practices forum gave a price of something like $60 per user interaction. Think of how much time your help desk will save with your new tool! Nicely done.

      • Rainer-Michael Kennedy October 25, 2016 / 4:15 am

        LOL What even…. oops! It was a long night!

        Many thanks, I will try this once I get home and let you know the results!

      • rainer-michael kennedy October 25, 2016 / 5:12 pm

        Hey Mate, it’s me again.

        I’ve taken the advise and got it working again, but then i decided to try and jazz up the UI, and it doesn’t work again! Nice aye! I get –
        “Exception calling ‘showdialog’ with ‘0’ argument(s): ‘ cannot set visibility or call show, showdialog, or windowinteropholper.ensurehandle after a window has close.”

        I’ve pasted my code again here http://pastebin.com/57jd9Vfx

        Appreciate the response.

        Rainer

        • rainer-michael kennedy October 25, 2016 / 5:18 pm

          Though, for trying to make it look nicer, it might be worth me making a CSS page that then runs powershell’s when buttons are pressed.

          Might be my best option.

        • FoxDeploy October 26, 2016 / 9:33 am

          the UI is consumed and deleted from memory after being displayed once. This means you have to rerun the whole script everytime you want to display the UI.

  2. Daniel Potter October 25, 2016 / 9:56 am

    This would be much simpler.

    Function Get-DiskInfo {

    param ($computername)

    Get-WMIObject Win32_logicaldisk -ComputerName $computername | ?{ $_.freespace }|% {

    [pscustomobject]@{
    computername = $computername
    ‘Drive Letter’ = $_.DeviceID
    ‘Drive Label’ = $_.VolumeName
    ‘Size(MB)’ = [int]($_.Size / 1MB)
    ‘FreeSpace%’ = [math]::Round($_.FreeSpace / $_.Size, 2) * 100
    }
    }

    }

    $form1_Load = {

    $textbox1.Text = $env:COMPUTERNAME
    }

    $button1_Click={

    $diskinfo = Get-DiskInfo $textbox1.text
    $datagridview1.DataSource=[collections.arraylist]$diskinfo
    }

  3. joe November 8, 2016 / 3:30 pm

    Where is part 3? I’m definitely interested in learning about having tabs in the gui

    • FoxDeploy November 8, 2016 / 6:49 pm

      If you click the banner up top, it will take you to the home page for GUI posts, the link for post three.

      • joe November 9, 2016 / 11:49 am

        ok I see it now. You should add a link to it at the bottom of the article! Also… I’m so happy I found this. I’m working on an app now. I’m still learning posh but i’m challenging myself and going to get it right. I want to know… is it possible to make it so a tab is only available when the actions of the second tab are complete? OR better yet… is there a way to make it so I can have a next button that bring the user to a new “slide” for lack of a better term?

        • FoxDeploy November 9, 2016 / 11:51 am

          That’s a good idea, I’ll add links!

          Also, you can do what you’re asking about I’ll whip up an example and send it over in a few hours

      • joe November 9, 2016 / 11:58 am

        Ok I see it now! You should add a link to it at the bottom of the article! Also… I’m so happy I found this. I’m working on an app now. I’m still learning posh but I’m challenging myself and going to get it right. I want to know… is it possible to make it so a tab is only available when the actions of the first tab are completed? OR better yet… is there a way to make it so I can have a next button that bring the user to a new “slide”/ “window” (for lack of a better term?) without having it open a separate window, rather, it just loads a new view on the same window?

        (PS… not spam… won’t let me delete other comment, realized it didn’t make sense lol)

        • FoxDeploy November 9, 2016 / 2:38 pm

          Here you go!

  4. Christoph Müller December 15, 2016 / 10:04 am

    Hello guys, how can I bring the window into the front, I tried different things like $Form.Activate() or $Form.IsActive=$true but nothing works.
    I guess the solution is super easy!?
    Please give me a tip.

    thanks a lot.

    • Mike December 15, 2016 / 3:31 pm

      $MainForm.StartPosition = “CenterScreen” where $MainForm is, of course the name of the main form.

      • Christoph Müller December 15, 2016 / 3:43 pm

        Property ‘CenterScreen’ cannot be found on this object.
        is this order right?

        $Form.StartPosition=”CenterScreen”
        $Form.ShowDialog() | out-null

        • Mike December 15, 2016 / 3:50 pm

          I have it located in the beginning of my properties of the form:
          $MainForm = New-Object System.Windows.Forms.Form
          $MainForm.AutoScroll = $True
          $MainForm.AutoSize = $True
          $MainForm.AutoSizeMode = “GrowAndShrink”
          $MainForm.StartPosition = “CenterScreen”

  5. Christoph Müller December 15, 2016 / 6:11 pm

    thanks, but in my case its another thing, I just inserted Topmost=”True” in the xaml code. now it comes into the front BUT if I open a Out-GridView from the window, this Out-gridview will be displayed behind the window. is there a workaround?

    • Christoph Müller December 16, 2016 / 10:13 am

      got it. 1st call when a button is clicked $Form.Topmost=$false.
      thank you guys. sometimes I need just to talk about with others and the solution comes by itself.

  6. Nick January 8, 2017 / 6:26 pm

    I’m not sure if my code below made it to my last comment so I’m reposting. Please let me know why it keeps erroring out.

    $inputXML = @”

    “@

    $inputXML = $inputXML -replace ‘mc:Ignorable=”d”‘,” -replace “x:N”,’N’ -replace ‘^<Win.*', '<Window'

    [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{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}

    #===========================================================================
    # Load XAML Objects In PowerShell
    #===========================================================================

    $xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name)}

    Function Get-FormVariables{
    if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
    write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
    get-variable WPF*
    }

    Get-FormVariables

    #===========================================================================
    # Actually make the objects work
    #===========================================================================

    #Sample entry of how to add data to a field

    #$vmpicklistView.items.Add([pscustomobject]@{'VMName'=($_).Name;Status=$_.Status;Other="Yes"})

    #===========================================================================
    # Shows the form
    #===========================================================================
    write-host "To show the form, run the following" -ForegroundColor Cyan
    '$Form.ShowDialog() | out-null'

    • FoxDeploy January 12, 2017 / 9:10 am

      Hi Nick, can you post the full code into PasteBin or a Gist and send me the link? Should be able to help you out 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s