Home ISESteroids Documentation Creating User Interfaces
 

Creating User Interfaces

Let’s create this window – in under 2 minutes – promise!

Sample Dialog

Before you head to the next section and discover how it’s done: PowerShell is an automation language and text based. Still, it can also serve as efficient application development language for IT tasks, and produce highly useful tools with graphical user interfaces.

At the end of this tutorial, you’ll design a service stopper tool and ship it as application. Just don’t overdo it. Large projects with concurrent tasks are better done in c#.

Creating Sample WPF Dialog Window

Getting started is the hard part, adjusting running code is way more fun. To produce the sample dialog above, in ISESteroids simply right-click any blank space in your editor. In the context menu, choose “WPF / Create Sample WPF Window”. The sample code is inserted, and you can immediately run the code.

Insert Sample WPF Code

You can understand the sample code best when you collapse all regions. Press CTRL+M.

Collapsing Regions is Broken?

Well, yes – sometimes. This is the only long-term bug we are still chasing. If at any time collapsing does not work right in ISESteroids, simply press CTRL+M twice. This will fix it. If you ever find reproducable steps that will always lead to the issue, please let us know! It seems to occur rather randomly.

WPF Overview

 

We’ll walk you through the code next. But relax: this code gets auto-generated for you. Not just in this sample. Also when you start to create very own windows. You just need to know the basic idea.

XAML Window Definition

The beauty about WPF is that it works similar to HTML: the user interface design is done in a markup language, so you do not need any programming skills. When you expand the section “XAML Window Definition”, you see the window layout:


#region XAML window definition
# Right-⁠click XAML and choose WPF/Edit... to edit WPF Design
# in your favorite WPF editing tool
$xaml = @'
<Window
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   MinWidth="200"
   Width ="400"
   SizeToContent="Height"
   Title="New Mail"
   Topmost="True">
   <Grid Margin="10,40,10,10">
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="Auto"/>
         <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="*"/>
      </Grid.RowDefinitions>
      <TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="5">Please enter your details:</TextBlock>

      <TextBlock Grid.Column="0" Grid.Row="1" Margin="5">Name</TextBlock>
      <TextBlock Grid.Column="0" Grid.Row="2" Margin="5">Email</TextBlock>
      <TextBox Name="TxtName" Grid.Column="1" Grid.Row="1" Margin="5"></TextBox>
      <TextBox Name="TxtEmail" Grid.Column="1" Grid.Row="2" Margin="5"></TextBox>

      <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,10,0,0" Grid.Row="3" Grid.ColumnSpan="2">
        <Button Name="ButOk" MinWidth="80" Height="22" Margin="5">OK</Button>
        <Button Name="ButCancel" MinWidth="80" Height="22" Margin="5">Cancel</Button>
      </StackPanel>
   </Grid>
</Window>
'@

#endregion

It has start and close tags, like <Window>…</Window>. These tags are case-sensitive because WPF is XML-based.

You also see all parts of your dialog window. We marked the spots in orange that define window texts. To tailor the dialog to your needs, just go ahead and change the orange parts to whatever you like. When done, hit F5 to run the code again, and the window reflects your changes. Adjusting WPF code is not hard at all.

How Elements are Organized

Unlike the older WinForm technology, WPF uses an invisible grid, much like an excel sheet, to place elements on the window. This is much easier and not pixel-oriented, so it works well for high resolution displays, too. Here is the grid definition:

      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="Auto"/>
         <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="*"/>
      </Grid.RowDefinitions>

The grid has two columns. The first column has a width of “Auto” (wide enough to fit everything in). The second column gets the remaining room.

And it has four rows. The first three are auto-sized, and the last gets the remaining room. Easy enough.

The actual user interface elements are now placed in these grid cells. Lets take a look:

      <TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="5">Please enter your details:</TextBlock>

      <TextBlock Grid.Column="0" Grid.Row="1" Margin="5">Name</TextBlock>
      <TextBlock Grid.Column="0" Grid.Row="2" Margin="5">Email</TextBlock>
      <TextBox Name="TxtName" Grid.Column="1" Grid.Row="1" Margin="5"></TextBox>
      <TextBox Name="TxtEmail" Grid.Column="1" Grid.Row="2" Margin="5"></TextBox>

There are user interface elements like “TextBlock” (text labels) and “TextBox” (input areas). They are positioned into the grid using Grid.Column and Grid.Row. Later, you will see how you can use Grid.HorizontalAlignment and Grid.VerticalAlignment to further finetune positioning.

Elements can be nested, too. The two buttons, for example, are placed into a “StackPanel”:

      <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,10,0,0" Grid.Row="3" Grid.ColumnSpan="2">
        <Button Name="ButOk" MinWidth="80" Height="22" Margin="5">OK</Button>
        <Button Name="ButCancel" MinWidth="80" Height="22" Margin="5">Cancel</Button>
      </StackPanel>

The “StackPanel” stacks its child elements, either horizontal or vertical, and again is placed into the master grid.

Code Behind

The XAML layout is just text. To turn it into a real window that you can show around, you need some code. That’s the code in the region “Code Behind”, and it looks like this:


#region Code Behind
function Convert-⁠XAMLtoWindow
{
   param
   (
     [Parameter(Mandatory)]
     [string]
     $XAML,

     [string[]]
     $NamedElement=$null,

     [switch]
     $PassThru
   )

   Add-⁠Type -⁠AssemblyName PresentationFramework

   $reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
   $result = [Windows.Markup.XAMLReader]::Load($reader)
   foreach($Name in $NamedElement)
   {
     $result | Add-⁠Member NoteProperty -⁠Name $Name -⁠Value $result.FindName($Name) -⁠Force
   }

   if ($PassThru)
   {
     $result
   }
   else
   {
     $null = $window.Dispatcher.InvokeAsync{
       $result = $window.ShowDialog()
       Set-⁠Variable -⁠Name result -⁠Value $result -⁠Scope 1
     }.Wait()
     $result
   }
}

function Show-⁠WPFWindow
{
   param
   (
     [Parameter(Mandatory)]
     [Windows.Window]
     $Window
   )

   $result = $null
   $null = $window.Dispatcher.InvokeAsync{
     $result = $window.ShowDialog()
     Set-⁠Variable -⁠Name result -⁠Value $result -⁠Scope 1
   }.Wait()
   $result
}
#endregion Code Behind

Basically, it defines the two functions Convert-XAMLtoWindow (which takes your XAML layout and produces a window object), and Show-WPFWindow (which shows the window to the user).

Convert XAML to Window

Once you have these functions, the rest is really easy. Here is how the XAML is turned into a real window:


#region Convert XAML to Window
$window = Convert-⁠XAMLtoWindow -⁠XAML $xaml -⁠NamedElement 'TxtName', 'TxtEmail', 'ButOk', 'ButCancel' -⁠PassThru
#endregion

Take a look at the parameter -NamedElements: it lists all UI elements in your XAML that have a “Name” attribute. This way, you later can directly access these elements via $window.TxtName, for example, to find out what someone entered in a textbox.

Define Event Handlers

XAML just defines the window design. It won’t do anything else. If you want your window to respond to events, such as clicking a button, you need to add event handler code. ISESteroids will do that for you (see below). Here are the event handlers in the sample code:


#region Define Event Handlers
# Right-⁠Click XAML Text and choose WPF/Attach Events to
# add more handlers
$window.ButCancel.add_Click(
   {
     $window.DialogResult = $false
   }
)

$window.ButOk.add_Click(
   {
     $window.DialogResult = $true
   }
)
#endregion Event Handlers

Basically, the two buttons get assigned a scriptblock each. The scriptblock executes whenever the “Click” event fires. Both event handlers don’t do much. They set the property DialogResult on the window, which closes the window and sets the return value. This way, the caller knows which button was clicked.

Manipulate Window Content

We are almost ready to show the window. Maybe you’d like to preset some things in your window. This is what this part does:


#region Manipulate Window Content
$window.TxtName.Text = $env:username
$window.TxtEmail.Text = '[email protected]'
$null = $window.TxtName.Focus()
#endregion

It prefills the two text boxes and sets the input focus to the first text box so that the user can happily start to enter his name into it.

Show Window

Showing the window at last is just a one-liner:


# Show Window
$result = Show-⁠WPFWindow -⁠Window $window

Note how the window in $window is submitted to Show-WPFWindow. The function returns the value set by the two button click handlers.

Processing Results

When the window is closed, the final part is to suck out the information from it, and use it for whatever you like. This is done in the last part:


#region Process results
if ($result -⁠eq $true)
{
   $hash = [Ordered]@{
     EmployeeName = $window.TxtName.Text
     EmployeeMail = $window.TxtEmail.Text
   }
   New-⁠Object -⁠TypeName PSObject -⁠Property $hash
}
else
{
   Write-⁠Warning 'User aborted dialog.'
}
#endregion Process results

It creates an ordered hashtable and defines two keys. Their values are defined by the “Text” property of each of the two “TextBox” input elements. The hashtable then is turned into an object and returned. This is what the result looks like:

PS C:\Users\tobwe> . 'C:\Users\tobwe\Documents\powershell\testwindow.ps1

EmployeeName EmployeeMail
------------ ------------
Tobias       [email protected]



PS C:\Users\tobwe>

Saving the Result

The sample code is just a script, but when you wrap a function around it, you can open the window as often as you want, and assign its return value to a variable:

WPF Function

WPF Tools in ISESteroids

When you look at the top of the XAML definition in ISESteroids, you’ll discover a couple of new clickable links:

WPF Tools

  • Edit: Opens a menu with WPF Designer Tools that help you graphically edit and design XAML
  • Code Behind: Ensures your WPF code is present and accurate. If not, the code is added or adjusted.
  • Events: lists the events a named UI element exposes, and adds all event handlers that you’d like to respond to

In addition, when you look at the XAML code inside ISESteroids, all UI element names are highlighted so you always know the names of elements that you can access from your code. Just make sure you assigned a name to each UI element that you want to “talk” to later.

Designing User Interfaces

While you can adjust and design your UI solely within the ISE editor, it often is much easier to do this with a graphical tool. ISESteroids ships with a special version of the free Kaxaml editor. Just click on Edit, then choose Kaxaml:

Start WPF Editor

Using Visual Studio for Advanced WPF Editing

As you can see, ISESteroids also supports VisualStudio for WPF editing – these options are disabled if VisualStudio was not found. Go grab yourself one of the free VisualStudio Express Editions and install them. We’ll go over VisualStudio integration in a second.

Once Kaxaml is loaded, it shows the XAML code in its lower pane, and a live preview in the upper pane. While Kaxaml is connected to ISESteroids, you will see a strong red line around your XAML. Stop working in ISESteroids, and focus on Kaxaml. All changes you do in Kaxaml are automatically synced back to ISESteroids once you close Kaxaml.

Kaxaml

Kaxaml does not support graphical rearrangement of UI elements, but you can use its XAML editor to safely change the XAML, get IntelliSense, and also meaningful error messages when you do something wrong.

Please note that Kaxaml takes about 2-3 seconds after your last XAML change to update the visual pane. Only when the visual pane is updated will your edits be accepted. Do not close Kaxaml before the visual pane has updated, or else you might lose your last edits.

Using VisualStudio for WPF Editing

If VisualStudio is installed on your system, you can use its excellent WPF designer instead of Kaxaml. In the list above, click “VisualStudio (1-way-sync).

VisualStudio launches, creates a sample project, and all you need to do is to open the project explorer (press CTRL+ALT+L if it is not open), then double-click the file MainWindow.xaml.

VisualStudio as WPF Designer

  1. Project Explorer (Open via CTRL+ALT+L). Double-Click MainWindow.xaml to open the WPF Designer
  2. WPF Designer. Shows the UI design. You can use the mouse to rearrange items.
  3. XAML View. Synchronizes with the WPF Designer, so matter in which one of the two you make changes, the other one reflects them, too.
  4. Properties of selected UI element. You can change properties here, too.
  5. Toolbox: Click to open a list of UI elements. Drag elements from the Toolbox onto your WPF Designer view to add more UI elements

It may be a bit overwhelming at first to work with VisualStudio, yet it is very powerful. Once your window is designed, save your work, then close VisualStudio.

ISESteroids automatically picks up the changes, so you can immediately continue your work there, or run your script to test-drive the latest UI changes you applied.

1-way sync does not (currently) sync in VS 2015

In the current version of ISESteroids, syncing back the changes from VisualStudio to ISESteroids in 1-way sync does not always work, apparently depending on the version of VisualStudio you use. As a temporary workaround, simply copy and paste the XAML code from the XAML view inside VisualStudio into the ISE editor.

2-way sync is not affected and works as expected.

Visual Studio 2-way Sync

In the “edit” context menu on top of your XAML code inside ISESteroids, there is another option called “Edit in VisualStudio (2-way sync). This option is available only when you have a licensed version of VisualStudio because it uses the Visual Studio automation model.

In this mode, you get a live dual sync, so you can work both in VisualStudio and ISESteroids at the same time, and both sync changes back and forth. In addition, 2-way sync starts automatically into the WPF designer.

However, there are cases where 2-way sync cannot launch. We are still investigating these cases, so if your VisualStudio won’t launch with 2-way sync, please stick to 1-way sync instead.

Attaching to Events

On top of your XAML code inside ISESteroids, you can click “Events” to open a dialog listing all named UI elements in your XAML layout.

Attaching Events

Let’s take the sample WPF code inserted earlier, and make sure the dialog box beeps whenever a key is pressed. Click “Events”. A dialog opens:

Select Event

  1. Select the named element that you want to attach to. Chose the first textbox named “TxtName”
  2. Select the event you want to subscribe to. Choose “TextChanged”.
  3. Click “Insert” to create the event handler.

ISESteroids automatically adds code similar to this:


$window.TxtName.add_TextChanged{
  # remove param() block if access to event information is not required
  param
  (
   [Parameter(Mandatory)][Object]$sender,
   [Parameter(Mandatory)][Windows.Controls.TextChangedEventArgs]$e
  )

  # add event code here
}

This scriptblock is executed now whenever the “TextChanged” event fires for the UI element with name “TxtName”. You can see that the even passes over some arguments, but if you just want to beep on every key press, change it like so:


$window.TxtName.add_TextChanged{
  [Console]::Beep() 
}

When you run your code and enter text into the first textbox, Windows beeps with every key press now – provided you turned on your speakers.

Things can be much more sophisticated, though. Let’s assume you want to enable the OK button only if someone actually enters a valid email address in the second textbox. Create another event handler, this time for TxtEmail, and subscribe again to the “TextChanged” event.


  $window.TxtEmail.add_TextChanged{
   $pattern = '^[a-⁠zA-⁠Z0-⁠9.!#$%&''*+/=?^_{|}~-][email protected][a-⁠zA-⁠Z0-⁠9-]+(?:\.[a-⁠zA-⁠Z0-⁠9-]+)*$'
   $window.ButOk.IsEnabled = $window.TxtEmail.Text -⁠match $pattern
  }

When you run your code, it works! The OK button is dimmed when no valid email address is entered into the second text box – except it turns out that the regular expression used in this example isn’t very strict. This is not WPF’s fault, though. You get the idea. From within an event handler, you can access any other named UI element and manipulate it.

Powerful Data Binding

Let’s do a real thing and create a dialog that can stop services. This is what it should look like:

Service Stopper

Here is the code – test yourself, it is pretty much the same approach as in the sample before:

#region XAML window definition
# Right-click XAML and choose WPF/Edit... to edit WPF Design
# in your favorite WPF editing tool
$xaml = @'
<Window
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   MinWidth="200"
   Width ="400"
   SizeToContent="Height"
   Title="Service Stopper"
   Topmost="True">
   <Grid Margin="10,40,10,10">
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="Auto"/>
         <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
      </Grid.RowDefinitions>
        <TextBlock Grid.Column="1" Margin="10">Choose Service to Stop:</TextBlock>

      <TextBlock Grid.Column="0" Grid.Row="1" Margin="5">Service</TextBlock>
      <ComboBox Name="ComboService" Grid.Column="1" Grid.Row="1" Margin="5"></ComboBox>
      
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,10,0,0" Grid.Row="2" Grid.ColumnSpan="2">
        <Button Name="ButOk" MinWidth="80" Height="22" Margin="5">Stop Service</Button>
        <Button Name="ButCancel" MinWidth="80" Height="22" Margin="5">Cancel</Button>
      </StackPanel>
   </Grid>
</Window>
'@


function Convert-XAMLtoWindow
{
   param
   (
    [Parameter(Mandatory)]
    [string]
    $XAML,

    [string[]]
    $NamedElement=$null,

    [switch]
    $PassThru
   )

   Add-Type -AssemblyName PresentationFramework

   $reader = [XML.XMLReader]::Create([System.IO.StringReader]$XAML)
   $result = [Windows.Markup.XAMLReader]::Load($reader)
   foreach($Name in $NamedElement)
   {
    $result | Add-Member NoteProperty -Name $Name -Value $result.FindName($Name) -Force
   }

   if ($PassThru)
   {
    $result
   }
   else
   {
    $null = $window.Dispatcher.InvokeAsync{
     $result = $window.ShowDialog()
     Set-Variable -Name result -Value $result -Scope 1
    }.Wait()
    $result
   }
}


function Show-WPFWindow
{
   param
   (
    [Parameter(Mandatory)]
    [Windows.Window]
    $Window
   )

   $result = $null
   $null = $window.Dispatcher.InvokeAsync{
    $result = $window.ShowDialog()
    Set-Variable -Name result -Value $result -Scope 1
   }.Wait()
   $result
}

$window = Convert-XAMLtoWindow -XAML $xaml -NamedElement 'ButCancel', 'ButOk', 'ComboService' -PassThru

# add click handlers
$window.ButOk.add_Click{
   # when clicked, take the selected item from the combo box and stop the service
   # use -whatif to just simulate for now
   $window.ComboService.SelectedItem | Stop-Service -WhatIf
   # update the combo box (if we really stopped a service, the list would now be shorter)
   $window.ComboService.ItemsSource = Get-Service | Where-Object Status -eq Running | Sort-Object -Property DisplayName
}

$window.ButCancel.add_Click{
   # close window
   $window.DialogResult = $false
}

# fill the combobox with some powershell objects
$window.ComboService.ItemsSource = Get-Service | Where-Object Status -eq Running | Sort-Object -Property DisplayName
# tell the combobox to use the property "DisplayName" to display the object in its list
$window.ComboService.DisplayMemberPath = 'DisplayName'
# tell the combobox to preselect the first element
$window.ComboService.SelectedIndex = 0

Show-WPFWindow -Window $window
#endregion

The new things are:

  • The XAML uses a new UI element called “ComboBox”
  • The combobox is filled with the results of a PowerShell command. It’s ridiculously easy to fill WPF elements this way – just tell them where their “ItemsSource” is.
  • The combobox can display any given property found in the objects you filled them with. Use “DisplayMemberPath” to specify the property.
  • The event handler for the OK button does not close the window. Instead, it performs some action (stops a service), then updates the combobox

The result is a quick and dirty yet powerful tool that can be used to stop services. With the same approach, you could have it restart services. You could limit the number of services before you make them available in the combobox. Or you could show disabled Active Directory users instead, and make someone unlock them.

Just remember: in the code above, we deliberately turned off the stop-service functionality with the parameter -whatIf. You need to remove this parameter from your code to actually stop services. Be careful! Some services don’t like to be stopped.

Creating a PowerShell Application (EXE)

To actually pass on your tool to someone else, you could decide to make it an application first. It’s dead simple. Just choose the menu “Tools / Turn Code into EXE”. A dialog opens. Here is what to choose:

Create EXE

  1. Make sure the PowerShell console is hidden. You just created a UI, so you don’t need the console anymore (except if you want to use it to output results of course)
  2. Select some icon for your application. That’s optional, but you could go to the windows folder and search for some *.ico files just for the fun of it
  3. Require Administrator privileges. Your tool needs them. A regular user cannot stop services.
  4. Click OK, and choose where to save your EXE file.

WPF EXE

When you launch your newly created application via double-click, Windows asks for elevation (because you required Administrator privileges). Next, your window pops up and shows the list of running services, and the option to stop some.

Adding Digital Signature

When you launch your application, Windows probably pops up a window first saying that this application is from an untrusted source. Which is correct. It protects you from hackers that steal benign icons and stick them to their malicious tools.

When you created the application, ISESteroids offered you to digitally sign the application. If you own a code signing certificate that is trusted on your machine, then use it to sign the application. This tells Windows that it comes from a trusted person, and the warning message no longer appears.