Tag Archives: ShowUI

Long-running background tasks in ShowUI GUIs from PowerShell

A lot of the time when you’re writing ShowUI user interfaces in PowerShell, you’re just asking users for inputs, prompting them for choices, or showing them the results of some calculations (whether that be dashboards, graphics, etc).

However, sometimes you need to prompt the user for input and then do some work, and you want to present the users with output as the work progresses. With any graphical user interface, when you want to do any significant amount of work, you need to do it on a background thread. The ShowUI way to do that is to use the Invoke-Background command, which gives you an event-driven way to run a script block (or command) and capture all of it’s various outputs.

I’ll show you an example here which writes output and progress events, and show you how to handle updating the user interface with both (hopefully you’ll be able to figure out the other output options, including the errors).


Import-Module ShowUI -Req 1.4

New-Grid -Columns "*","5","70" -Rows "21","Auto","5","*" -Margin 5 {
  ProgressBar -Name ProgressBar -ColumnSpan 3 -Maximum 100 -Margin "0,0,0,5"

  TextBox -Name count -Text 12 -MinWidth 150 -Row 1

  TextBox -Name output -MinHeight 100 -ColumnSpan 3 -IsReadOnly -Row 3

  Button "Start" -Name StartButton -Column 2 -Row 1 -On_Click {
    # If you need to pass values from your form to the background function
    # They need to be serializable, and go through the -Parameter hashtable:
    Invoke-Background -Parameter @{ work = [int]$count.Text } {
      param($work=10)
      # You would do real work here, but I'll just fake it
      foreach($item in 1..$work) {
        # Anything that goes to Output triggers the OutputChanged
        Write-Output "Processing item $item of $work"
        Start-Sleep -milli 500
        # And of course, progress triggers the ProgressChanged
        $progress = (([double]$item / [double]$work) * 100)
        Write-Progress "Processing" -PercentComplete $progress
      }
    } -On_OutputChanged {
      # The actual event is on the DataContext object
      $output.Text = $args[0].DataContext.Output -join "`n" | Out-String
    } -On_ProgressChanged {
      $ProgressBar.Value = $args[0].DataContext.LastProgress.PercentComplete
    }
  }
} -Show
 

The first key, of course, is knowing that Invoke-Background sets a DataContext on the object (either the object specified to the -Control parameter, or the parent who’s event handler calls it). That DataContext is a ShowUI.PowerShellDataSource (it’s code is in the C# folder in the ShowUI module), and it has events and properties for each of the streams: Output, Error, Warning, Verbose, Debug, and Progress. It also has a property for the “Last” item from each of those streams, and a TimeStampedOutput property which collects all of the output, in order, with a NoteProperty for the Stream and the TimeStamp. More importantly, it has events which fire on every output.

The second key is knowing that the properties like “Output” contain all the output, and properties like “LastOutput” contain only the very last thing output. Since in the case of the progress we only care about the very last value, we’re able to use the “LastProgress” property. Since we want to show all of the output, rather than try to collect the “last” one each time, we can just overwrite the textbox’s text with the output array.

It’s also very important to remember that the properties are arrays, and in particular to remember that the Output is an array of PSObject, and the others are arrays of ProgressRecords, DebugRecords, ErrorRecords, etc… so we use Out-String to make sure that we convert them to a string before we set the Text of our output pane.

Hopefully this function will help you out — I’ll try to improve it’s help and examples in the next release of ShowUI.

Rich formatting for PowerShell help

[updated] Ok, I just updated this with a new post on PoshCode. I posted the HtmlHelp module to PoshCode for generating HTML web pages based on the help for functions or cmdlets. It basically has one command: Get-HtmlHelp, which takes a couple of parameters now. The only mandatory parameter is the name of the command you want help for, in this case the html is output on the pipeline and can be redirected into a file.

Get-HtmlHelp Get-HtmlHelp | Set-Content Get-HtmlHelp.html
 

The markup generated is (I hope) reasonable and lightweight, with some simple css rules pre-applied. Feel free to customize the script to generate help however you like.

[new] Generating many at once

I forgot to mention the other parameters on Get-HtmlHelp. They’re pretty cool, because if you want to upload your help you can do so with this. Say you created a module, and you wanted to generate all the help into files for uploading. You want to set the -BaseUrl to the location you will upload them to, and then use the -OutputFolder parameter to generate an html file for each command into the specified folder:

Get-Command -Module ModuleName |
Get-HtmlHelp -BaseUrl http://HuddledMasses.org/HtmlHelp/ -OutputFolder ~\sites\HuddledMasses\HtmlHelp\
 

Now you can just take those files and upload them to the right spot on your website. I actually have some scripts which I can wrap around this to post the help to a wiki, but you’re just going to have to wait for that until the next time I get inspired to work on help …

Show-Help

I did include a little function in the comments and in the help for Get-HtmlHelp which uses ShowUI to display the rich formatted help in a popup window:

function Show-Help {
[CmdletBinding()]
param([String]$Name)  
   Window { WebBrowser -Name wb } -On_Loaded {
      $wb.NavigateToString((Get-HtmlHelp $Name))
      $this.Title = "Get-Help $Name"
   } -Show
}
 

So anyway, enough of that. When you run it, it looks like this (click for the full screenshot):

Click to see full screen-shot

More ShowUI Widgets

Well, I got so many tweets and comments on IRC about my ShowUI clock that I figured I’d share a few other widgets I had lying around … but first, an update to New-UIWidget:


function New-UIWidget {
[CmdletBinding()]
param(
    [ScriptBlock]$Content,
    [Alias("Refresh")]
    [TimeSpan]$Interval = "0:0:2",
    [ScriptBlock]$UpdateBlock,
    [Switch]$Show, [Switch]$AsJob
)

$WidgetValues = @{
    AllowsTransparency = $true
    WindowStyle = "None"
    ShowInTaskbar = $true
    Background = "Transparent"
    On_MouseLeftButtonDown = { $Window.DragMove() }
    On_Closing = { $Window.Resources.Timers."Clock".Stop() }
    Tag = @{"UpdateBlock"=$UpdateBlock;"Interval"=$Interval}
    SizeToContent = "WidthAndHeight"
    ResizeMode = "NoResize"
    On_SourceInitialized = {
        $Window.Resources.Timers.Clock = (New-Object Windows.Threading.DispatcherTimer).PSObject.BaseObject
        $Window.Resources.Timers.Clock.Interval = $Window.Tag.Interval
        Add-EventHandler $Window.Resources.Timers.Clock Tick $Window.Tag.UpdateBlock
        $Window.Resources.Timers.Clock.Start()
        $Window.Tag = $Null
    }
    On_ContentRendered = $UpdateBlock  
}
New-Window @WidgetValues -Content $Content -Show:$Show -AsJob:$AsJob
}
 

Basically I just switched it to use Add-EventHandler, which in ShowUI does at lot of awesome magic to make sure all the variables you would want (i.e.: variables for named controls) are defined in your event handler. It makes writing these gadgets much easier. Here are a few:

Let’s start with the simplest Widget I can dream up:

New-UIWidget { Label -Name Time -FontSize 48 } "0:0:0.2" { $Time.Content = Get-Date -f "h:mm tt"}

Yeah, just a simple text clock. To be honest, that’s a bit plain for me … but I do like digital.

Another few Clocks

My preference is for something very large, with the date and stuff on it:

If you prefer analog clocks, this one has “hands” and old school stopped movement (no click sound, but maybe you could add that)...

And finally, while I’m on the subject, the clock from yesterday could be a lot simpler if it didn’t need animation, here’s the target part of it. Feel free to explore.

And now for something completely different

My other favorite widget to implement in new UI frameworks is a weather widget. Why? Well, mostly because it requires parsing an RSS feed and a few extra pieces of work that let you actually determine how hard it’s going to be to write more advanced UI. In any case, here’s my ShowUI weather widget (note that you have to change the $woEID to get your forecast):

OK, that’s enough for one night, but I’ll take requests for tomorrow! As a side note, you know the cool thing about these gadgets? When I took the screenshots, those were all running from the same PowerShell window -AsJobs, and using less than 1% CPU.

The Obligatory ShowUI Clock

Well, it all started when Richard Siddaway posted his PowerShell Clock using raw XAML. Then Doug posted his ShowUI Clock, and after some back and forth on our ShowUI developer mailing list, James Brundage posted a video walkthrough of building his clock

Now I’ve dug an old clock of mine out of mothballs and updated it to run on ShowUI — I believe that I’ve never posted this script before, so I don’t feel bad at all doing so now, even though I actually wrote it last December (hey, look, I have a screenshot against my bright red Christmas wallpaper to prove it — totally random, I know). Anyway, here’s the deal: this post was supposed to be written 6 months ago, about building fun UI Widgets in PowerShell, but I never got to it, because we started working on the PowerBoots + WPK merger we call ShowUI. So here’s a fun little function for you:


function New-UIWidget {
param(
    [ScriptBlock]$Content,
    [TimeSpan]$Interval = "0:0:2",
    [ScriptBlock]$UpdateBlock,
    [Switch]$Show, [Switch]$AsJob
)

$WidgetValues = @{
    AllowsTransparency = $true
    WindowStyle = "None"
    ShowInTaskbar = $true
    Background = "Transparent"
    On_MouseLeftButtonDown = { $Window.DragMove() }
    On_Closing = { $Window.Resources.Timers."Clock".Stop() }
    Tag = $UpdateBlock,$Interval
    SizeToContent = "WidthAndHeight"
    ResizeMode = "NoResize"
    On_SourceInitialized = {
        $Window.Resources.Timers."Clock" = (New-Object Windows.Threading.DispatcherTimer).PSObject.BaseObject
        $Window.Resources.Timers."Clock".Interval = $Window.Tag[1]
        $Window.Resources.Timers."Clock".Add_Tick( $Window.Tag[0] )
        $Window.Resources.Timers."Clock".Tag = $Window
        $Window.Resources.Timers."Clock".Start()
        $Window.Tag = $Null
    }
}

New-Window @WidgetValues -Content $Content -Show:$Show -AsJob:$AsJob
}
 

That script is what I use to create little “gadget” style windows that are transparent, don’t show up on the taskbar, but are draggable, and have an automatic update based on a timer. Now, this is a very simple version of the gadgets, without using our PowerShell “DataSource” or any data binding at all. I intend to update this with more functionality one of these days, but this was enough to make my clock work. You can see that basically all it does is sets a bunch of parameters for the Window, including a SourceInitialized event handler, and all the stuff necessary to make it transparent and draggable.

Now let me post the New-TargetClock funtion, which depends on that function, and we’ll get to explaining it a bit down below. First, the screenshot, this is what it looks like (with a PowerShell window behind it, instead of that bright red wallpaper):

And then the actual function:


function New-TargetClock {
param([Switch]$ShowMarks)

New-UIWidget -AsJob -Content {
    Grid {
        $now = Get-Date;
        $Label = @{HorizontalAlignment="Center"; VerticalAlignment="Top";FontSize=25;ZIndex=1}
        # Hours
        TextBlock -Text H -Margin '0,-5,0,0' -FontWeight ExtraBold -Foreground White @Label
        Ellipse -Name Hours -Ov el -Fill Transparent -Stroke Black `
                -StrokeThickness 100 -Width 350 -Height 350 `
                -StrokeDashArray  7.85, 7.85  -Opacity 0.5 `
                -StrokeDashOffset $(7.85 - (7.85 * ((($now.Hour % 12) + ($now.Minute/60))/12))) `
                -RenderTransformOrigin "0.5,0.5" -RenderTransform { RotateTransform -Angle -90 }
            $animate = DoubleAnimation -To 0 -Duration "$(11 - ($now.Hour % 12)):$(59 - $now.Minute):$(60 - $now.Second)"  
            $el[0].BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )
       
        # Minute
        TextBlock -Text M  -Margin '0,20,0,0' -FontWeight ExtraBold -Foreground White @Label
        Ellipse -Name Minutes -Ov el -Fill Transparent -Stroke Gray `
                -StrokeThickness 75  -Width 300 -Height 300 `
                -StrokeDashArray  9.43, 9.43 `
                -StrokeDashOffset $(9.43 - (9.43 * (($now.Minute) + ($now.Second/60))/60)) `
                -RenderTransformOrigin "0.5,0.5" -RenderTransform { RotateTransform -Angle -90 }
            $animate = DoubleAnimation -To 0 -Duration "00:$(59 - $now.Minute):$(60 - $now.Second)"
            $el[0].BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )
         
        # Seconds
        TextBlock -Text S -Margin '0,45,0,0' -FontWeight ExtraBold -Foreground Gray @Label
        Ellipse -Name Seconds -Ov el -Fill Transparent -Stroke White `
                -StrokeThickness 50  -Width 250 -Height 250 `
                -StrokeDashArray 12.57,12.57 `
                -StrokeDashOffset $(12.57 - (12.57 * ($now.Second/60))) `
                -RenderTransformOrigin "0.5,0.5" -RenderTransform { RotateTransform -Angle -90 }
            $animate = DoubleAnimation -To 0 -Duration "00:00:$(60 - $now.Second)"
            $el[0].BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )

        ## These go in the center of the circles (notice the alignment)
        $Centered = @{HorizontalAlignment="Center";VerticalAlignment="Center"}
        StackPanel @Centered {
            TextBlock -Name Time -FontWeight ExtraBold -Foreground Black -FontSize 30 `
                      -Text $Now.ToString("h:mm") @Centered
            TextBlock -Name Day -FontWeight Normal -Foreground Black -FontSize 23 `
                      -Text $Now.DayOfWeek.ToString().ToUpper() @Centered
            StackPanel -Orientation Horizontal @Centered {
                TextBlock -Name Date -FontWeight ExtraBold -Foreground Black -FontSize 18 `
                          -Text $Now.Day -Margin '0,0,5,0'
                TextBlock -Name Month -FontWeight ExtraBold -Foreground White -FontSize 18 `
                          -Text $Now.ToString("MMMM").ToUpper()
            }
        }

        ## Hour marks
        if($ShowMarks) {
            Ellipse -Width 350 -Height 350 -StrokeThickness 5 -Fill Transparent -Stroke White -StrokeDashArray 0.5,17.6 -RenderTransformOrigin "0.5,0.5" -RenderTransform {RotateTransform -Angle -90.5} -Opacity 0.8
            Ellipse -Width 160 -Height 160 -StrokeThickness 5 -Fill Transparent -Stroke Black -StrokeDashArray 0.5,7.62 -RenderTransformOrigin "0.5,0.5" -RenderTransform {RotateTransform -Angle -90.5} -Opacity 0.8
        }
    }
} -UpdateBlock {
    $children = @{}
    foreach($child in Get-ChildControl -Control $this.Tag){
        $children.($child.Name) = $child
    }

   $now = Get-Date

    if($now.Second -eq 0) {
        if($children.Seconds.Tag -ne $true) {
            $children.Seconds.Tag  = $true
            $animate = DoubleAnimation -From $children.Seconds.StrokeDashArray[0] -To 0 -Duration '00:01' -RepeatBehavior ([system.windows.media.animation.RepeatBehavior]::Forever)
            $children.Seconds.BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )
        }
    } else {
        $children.Seconds.Tag  = $false
    }
   
    if($now.Minute -eq 0) {
        if($children.Minutes.Tag -ne $true) {
            $children.Minutes.Tag = $true
            $animate = DoubleAnimation -From $children.Minutes.StrokeDashArray[0] -To 0 -Duration '01:00' -RepeatBehavior ([system.windows.media.animation.RepeatBehavior]::Forever)
            $children.Minutes.BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )
        }
    } else {
        $children.Minutes.Tag  = $false
    }
   
    if($now.Hour -eq 12) {
        if($children.Hours.Tag -ne $true) {
            $children.Hours.Tag  = $true
            $animate = DoubleAnimation -From $children.Hours.StrokeDashArray[0] -To 0 -Duration '12:00' -RepeatBehavior ([system.windows.media.animation.RepeatBehavior]::Forever)
            $children.Hours.BeginAnimation( [System.Windows.Shapes.Shape]::StrokeDashOffsetProperty, $animate )
        }
    } else {
        $children.Hours.Tag  = $false
    }

    $children.Time.Text = $Now.ToString("h:mm")
    $children.Day.Text = $Now.DayOfWeek.ToString().ToUpper()
    $children.Date.Text = $Now.Day
    $children.Month.Text = $Now.ToString("MMMM").ToUpper()
}
}
 

What do you think of that?

The clever part (which, believe me, took a LOT of experimenting), is that I’m creating a StrokeDashArray and animating the StrokeDashOffsetProperty in such a way that the “stroke” of the Ellipse (the circle of the clock) gets drawn slowly over time (very slowly, in the case of the hour “hand”). Now, as you can see, I’m still setting up the animations by hand (I just didn’t convert the code to use Start-Animation), and in the UpdateBlock, I actually reset the animations if they’re at zero. I can write more about this if people have questions, but it’s nearly 2:30am, so I’ll take questions tomorrow :)

Disabling Events in ShowUI

Because some things just work better in WPF.

Let’s say you had a form that collected a bunch of user input, and then had a button that would fire off some work. We’ll assume that you wanted to prevent people from firing off the work again before they know the results of the first time, so you’ll disable the button while you’re working. Something like this (leaving out the boring stuff about collecting the user’s input and displaying the results):


Import-Module ShowUI

StackPanel {
   TextBlock -Name Output
   Button "Click Me" -Margin 3 -On_Click {
      $this.IsEnabled = $False
      # Update the user about what we're doing:
      $Output.Text += "We are doing some hard work...`n"
     
      # Simulate doing some hard work ...
      $times = $times + 1
      Start-Sleep -Seconds 3
     
      # Let them know about all our hard work
      $Output.Text += "Work completed $times time(s).`n"
      $this.IsEnabled = $True
   }
} -Show
 

It’s pretty simple. In fact, I think there’s only three things worth pointing out:

  1. There are variables for named controls.
  2. If you want to make sure a control has time to update it’s display while you’re executing your event handler, call UpdateLayout().
  1. You can disable and enable buttons.

Now, try doing the same thing with Windows Forms.

This was my first attempt:


[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")

# Create the form
$form = new-object System.Windows.Forms.Form
$form.Size = "250,250"

## Create text
$output = new-object system.windows.forms.label
$output.Anchor = "Top,Left,Right"
$output.AutoSize = $True

## Create button
$button = new-object system.windows.forms.button
$button.Anchor = "Bottom,Right"
$button.Text = "Click Me"
$button.Location = "150,180"

$button.Add_Click({
    $button.Enabled = $False
    $Output.Text += "We are doing some hard work...`n"
   
    ## Simulate doing work
    $times = $times + 1
    Start-Sleep -Seconds 5
     
    # Let them know about all our hard work
    $Output.Text += "Work completed $times time(s).`n"
    $button.Enabled = $True
})

$form.Controls.AddRange(@($output, $button))

## show the form
[void]$form.showdialog()
 

Now, I bet you noticed that took a bit more code than the WPF version in ShowUI, but the thing about it is that I also had to manually specify the positions of the controls (and I had to set the size of the form to make that work). And after all that, it still doesn’t actually work, and it has a few ugly behaviors:

First of all, disabling the control doesn’t work, because Windows posts all click events as messages to the button’s message queue, and it doesn’t know that it should ignore them, because it only processes one at a time — when it’s done running the Click handler the first time, it notices there’s another event … and since it’s enabled, it processes it. If you click it five times while it’s disabled, you’re going to get the work done five more times… eventually.

On top of that, although the label updates without being told to, it didn’t push the button down the screen or resize the form, so eventually it overlaps the button, passes through the bottom of the form and disappears.

The next thing I tried was to actually unhook the event handler before I disable the button … but that had no effect either, because (as I wrote earlier) the button doesn’t actually ignore the clicks while it’s doing the work — it just queues them up for processing later.

Of course, I could do the work in a background job so that the button would return immediately, but that doesn’t meet the requirements I have for not queuing up extra work before the first work is done, and in fact, then I’d have extra work to do to create a time to check and see if the remote job was finished or not.

[New] Thanks to SAPIENDavid, I’ve realized the simple fix for the disabling problem, we just have to call DoEvents to empty the event queue before we re-enable the button.

Here’s what did work:


Add-Type -Assembly System.Windows.Forms

# Create the form
$form = New-Object System.Windows.Forms.Form -Property @{
    Size = "200,70"
    AutoSize = $true
}

# Create a FlowLayout
$panel = New-Object System.Windows.Forms.FlowLayoutPanel -Property @{
    Dock = "Fill"
    FlowDirection = "TopDown"
    AutoSize = $true
}

## Create text
$output = New-Object system.windows.forms.label -Property @{
    Dock = "Fill"
    Text = "Click the button when you're ready to work.`n"
    AutoSize = $true
}

## Create button
$button = New-Object system.windows.forms.button -Property @{
    Anchor = "Bottom,Right"
    Text = "Click Me"
}

$lastButtonClick = get-date
$button.Add_Click({
    $button.Enabled = $False
    $Output.Text += "We are doing some hard work...`n"
       
    ## Simulate doing work
    $times = $times + 1
    Start-Sleep -Seconds 5
         
    # Let them know about all our hard work
    $Output.Text += "Work completed $times time(s).`n"

    #Process the pending messages before enabling the button
    [System.Windows.Forms.Application]::DoEvents()
    $button.Enabled = $True
})

$panel.Controls.AddRange(@($output, $button))
$form.Controls.AddRange(@($panel))

## show the form
[void]$form.showdialog()
 

To fix the other problems, I added a FlowLayoutPanel (which is very different from the StackPanel in WPF, but still serves the same purpose), and made all the relevant bits autosize (it’s always surprising to me when I need to drop back to WinForms, how everything has to be told to autosize and fill empty space).

That’s enough to take care of the output problem, and make the two solutions roughly equivalent (there’s still some differences, as you can see if you run them).

For what it’s worth, this was a real world question from a user at our Virtual User Group this morning, and I just couldn’t help sharing how much easier user interfaces are to write in ShowUI. Clearly a designer like PrimalForms makes laying out the controls easier — but when it comes to the little things, you still have to figure out to make them work, and actually implement your event handlers correctly.

Just for the record, here’s what it would take to implement the WPF solution without ShowUI:


Add-Type -Assembly PresentationFramework
$window = New-Object System.Windows.Window -Property @{
    SizeToContent = 'WidthAndHeight'
    Content = New-Object System.Windows.Controls.StackPanel
}
$output = New-Object System.Windows.Controls.TextBlock
$button = New-Object System.Windows.Controls.Button -Property @{
    Content = "Click Me"
    Margin = 3
}

$button.Add_Click({
    $this.IsEnabled = $False
    # Update the user about what we're doing:
    $Output.Text += "We are doing some hard work...`n"
    $Window.UpdateLayout()
    # Simulate doing some hard work ...
    $times = $times + 1
    Start-Sleep -Seconds 3
    # Let them know about all our hard work
    $Output.Text += "Work completed $times time(s).`n"
    $this.IsEnabled = $True
})

$window.Content.Children.Add($output)
$window.Content.Children.Add($button)
$window.ShowDialog()
 

ShowUI: Handling Events and Producing Output

Once you get past the basics of WPF and ShowUI, learning to use nested panels or grids to achieve the layouts you want, and start getting a grip on what controls are available by default, the next step to building useful user interfaces is going to be handling user interactions.

In programming, we call those interactions “events.” An event in WPF covers all forms of user interactions like clicking a button, type in a textbox, or select items from a treeview or listbox, and even less obvious things like when the right or left mouse button get’s pressed down, or let up … or when the mouse moves, of when the control gets focus or looses it, or “touch” and “stylus” events, drag-and-drop events, etc. There are also events that aren’t caused by users, like events for when databinding is updated, when the control is initialized, hidden, made visible, etc.

In ShowUI, all events are handled by assigning scriptblocks to a parameter who’s name starts with “On_” like -On_Click or -On_GotFocus or -On_MouseLeftButtonDown or -On_TextInput … and so on.

Let’s say that you want a quick dialog like this:

You’re going to need to handle the OK button click, of course, but in that scriptblock, you’re going to want to get the text from the textbox, and make sure that it gets returned when the window is closed … and you’re going to want to close the Window!

We’re here to help! Within the event handler script blocks, ShowUI defines a bunch of variables for you to help you handle the event: $this is the source of the event, $_ is the event arguments, and $window is the top-level window that contains your UI. Any named controls in your script are also exposed as variables, so if you started with this script:


StackPanel -ControlName "Prompt" -Margin "8,0,8,8" {
    Label "Please Enter Your Full Name:"
    StackPanel -Orientation Horizontal {
        TextBox -Name FullName -Width 100
        Button "OK" -IsDefault -Width 50 -Margin "8,0,0,0" -On_Click {
            # Do something to output the name!
        }
    }
} -Show
 

You’re going to be able to get the text from the TextBox using $FullName.Text because you know the TextBox has a property named “Text” (since it’s exposed as a parameter for you in the TextBox command), and because now you know that ShowUI creates a variable for all the named controls.

In order to write output from a script like that one, you have to set the Tag property on the top level control (in this case, the StackPanel, which is obviously named “Prompt”). You can do that easily by hand, or you can use the Set-UIValue function.

In order to close the window, you’re going to have to do one of two things: first, you can use the handy Close-Control function, or you can call the Close method on the window. The Close-Control function will look at the “parent” (and it’s parent) to try and find the window that needs to be closed — but if it can’t find one, it will just hide the parent, so if your button were several layers deep (unlike ours), you’d have to specify the top level control as a parameter.

Here’s two versions of what it could have looked like when I was finished:


StackPanel -ControlName "Prompt" -Margin "8,0,8,8" {
    Label "Please Enter Your Full Name:"
    StackPanel -Orientation Horizontal {
        TextBox -Name FullName -Width 100
        Button "OK" -IsDefault -Width 50 -Margin "8,0,0,0" -On_Click {
            $Prompt.Tag = $FullName.Text
            $Window.Close()
        }
    }
} -On_Loaded { $FullName.Focus() } -Show
 

StackPanel -ControlName "Prompt" -Margin "8,0,8,8" {
    Label "Please Enter Your Full Name:"
    StackPanel -Orientation Horizontal {
        TextBox -Name FullName -Width 100
        Button "OK" -IsDefault -Width 50 -Margin "8,0,0,0" -On_Click {
            Set-UIValue $Prompt -Passthru | Close-Control
        }
    }
} -On_Loaded { $FullName.Focus() } -Show
 

One thing you’ll notice right away is that I cheated and actually added another event handler too: For the “Loaded” event on the StackPanel. This event handler is called during the initialization of the user interface, and gives you a chance to do things like what I did here: set the initial keyboard focus where you want it (so the user can start typing as soon as the window pops up).

However, if you run them both, you’ll notice another thing: the output is different. In the second example I took advantage of the fact that Set-UIValue will actually call Get-UIValue if you don’t pass it a parameter! The cool thing about Get-UIValue is that if it doesn’t find a “Tag” on the specified control, it will look through the children to find one, and create a hashtable out of the values it finds. So in this case, rather than write the code to get the value from the right textbox and set it myself, I just let the built-in features of ShowUI do their thing.

A bigger example

Of course, in neither example did I need to do anything with the button or with the actual parameters that are passed in to the button’s “Click” event … so perhaps one last (more complicated) example would be useful:


New-Grid -ControlName SelectUserGroups -Columns Auto,* -Rows 4 {
    $GetGroups = {
        $user = Get-QADUuser $this.Text -SizeLimit 1
        if($User.LogonName -eq $this.Text -or $User.Email -eq $this.Text) {
            $this.Foreground = "Black"
            $Group.ItemsSource = Get-QADGroup -ContainsMember $user
            $UserName.Text = $user.LogonName
            $EmailAddress.Text = $user.Email
        } else {
            $this.Foreground = "Red"
            $Group.ItemsSource = @()        
        }
    }
   
    New-Label "Name"
    New-Textbox -name UserName -minwidth 100 -Column 1 -On_LostFocus $GetGroups
   
    New-Label "Email" -Row 1
    New-Textbox -name EmailAddress -minwidth 100  -Column 1 -Row 1  -On_LostFocus $GetGroups
   
    New-Label "Group" -Row 2
    New-Listbox -name Group -Column 1 -Row 2
   
    New-Button "OK" -Row 3 -Column 1 -On_Click { Get-ParentControl | Set-UIValue -Passthru | Close-Control }
} -Show
 

Hopefully, by now this doesn’t need a whole lot of explanation, but let’s walk through it anyway. First of all, if you’re not familiar with them, this script uses the excellent PowerShell Commands for Active Directory from Quest software. This doesn’t represent a full, complete, useful user interface — it’s more of an example that you can hopefully work from (and please, contribute ammendments on PoshCode).

You can see that I used a Grid with four rows and two Columns: with one column “Auto“sized, and one using up all the rest of the UI (you can resize the window to make the fields bigger). The first thing in the grid is the definition of a scripblock which I assigned to a variable $GetGroups. The reason I did that is because I wanted to reuse the scriptblock as the event handler for both the UserName and EmailAddress fields.

Without dwelling a whole lot on it, you can see the last line is the “OK” button which get’s the parent grid and calls Set-UIValue to invoke, as before, the hashtable collection of all the textbox values.

The interesting stuff is in that GetGroup event handler:


    $user = Get-QADUuser $this.Text -SizeLimit 1
    if($User.LogonName -eq $this.Text -or $User.Email -eq $this.Text) {
        $this.Foreground = "Black"
        $Group.ItemsSource = Get-QADGroup -ContainsMember $user
        $UserName.Text = $user.LogonName
        $EmailAddress.Text = $user.Email
    } else {
        $this.Foreground = "Red"
        $Group.ItemsSource = @()        
    }
 

You can see that first we call Get-QADUser with the Text of $this field. By using $this, we make it so the event handler will work on both Textboxes, since it will get the text of whatever triggered the event handler. Get-QADUser doesn’t return anything unless it finds a user, and setting the SizeLimit ensures that we won’t end up waiting for it to retrieve “all” the users just because the Textbox was left empty. In fact, the point of that line is to make sure that we matched a user.

On the next line, I’m making sure that either the LogonName or Email of the user that we found matches fully the text that the user typed. This makes sure that the user that we matched is a full match, so we know we’ve gotten the person typing into the form to type a full, complete username or email address.

When it does match, we set the color to Black (in case it was an error and set to Red before), and we call Get-QADGroup to get all the groups that the user is a member of. We set those groups as the source for the $Group listbox, and they’ll immediately show up for the user. And finally, we update the few fields we’re showing from the user object we retrieved earlier.

Of course, when it doesn’t match, we set the text red to indicate an error, and then we zero out the data on the group listbox.

I hope this has helped some of you figure out event handlers in ShowUI — please feel free to ask questions in the comments below or on the ShowUI discussion boards.

ShowUI: the tutorial walkthrough

Ok, let’s be clear: you’ve seen this before, and this isn’t going to be the last you’ll see of it, because we’ve got much more in store in ShowUI, but for the first release of ShowUI, it’s obviously time to update this simple walkthrough of building simple user interfaces in PowerShell!

An introduction to ShowUI

ShowUI is the next generation PowerShell module for building user interfaces in script. It’s the merger of my previous PowerShell UI project (called PowerBoots) with one by James Brundage, former Microsoft PowerShell team member and founder of Start-Automating (called WPK) which shipped in the Windows 7 resource kit.

If you want to follow along, you need to:

1. Get a copy of the ShowUI Module from CodePlex
2. Install it by putting the “ShowUI” folder in one of your “Modules” folders (list them by typing $Env:PSMODULEPATH in PowerShell v2, and feel free to create the one you want if it doesn’t exist). You should end up with something like C:\Users\Jaykul\Documents\WindowsPowerShell\Modules\ShowUI\ShowUI.psm1
3. Run PowerShell ISE, or use PowerShell.exe with the -STA switch (the best way to do this is to add it to the shortcut you use to launch PowerShell: open the properties dialog, and on the Shortcut tab, add “ -STA” to the Target)1.

Did I hear someone ask what is WPF? It was introduced as part of .Net 3.0 (and vastly improved in .Net 3.5, and again in 4.0), so you can expect to find it preinstalled on computers from Vista on, and of course you can download and install it on XP if it’s not already installed. The only thing you really need to know about WPF for the purposes of this tutorial is that it is the new GUI toolkit for .Net, and that it is container based — you put elements into other elements to control the layout, rather like HTML and Java Swing… you can pick up the rest as we go along.

A simple ShowUI program


New-Button -Content "Hello World" -Show
 

This first example is a little bit more verbose than it needs to be, because the -Content parameter is positional, so the first non-named argument you pass will be used for that. The same is true for the -Children parameter of panels, and in fact, each of the other similar content parameters: Items, Blocks, and Inlines.

Additionally, each control is aliased without the New- verb, so you could just call Button instead of New-Button … and of course, since our button doesn’t do anything, we could just as easily have used a Label, and written:


Label "Hello World" -Show
 

Note: “Label” is also the name of an executable for labelling drives in Windows, make sure ShowUI is imported before you run that command.

One last point: ShowUI brings a lot of features from WPK to the table, and one you’ll use a lot is the ability to skip specifying the window and put the -Show parameter on almost any WPF element. This is partly because we’re back to running PowerShell 2.0 with the -STA switch, but in any case, we can now skip the “New-BootsWindow” command, and our examples can be that much simpler.

We can put controls in a stack


Show-UI {
    StackPanel {
        Button "A bed of clams"
        Button "A coalition of cheetas"
        Button "A gulp of swallows"
    }
}
 

StackPanels are awesome. So are WrapPanels. Try that code with a WrapPanel instead of a StackPanel and see what the difference is (hint: try resizing the window). Actually, try it with a UniformGrid too. WPF has several other panel containers too: Grid, ToolBarPanel, TabPanel, DockPanel, Canvas … we’ll talk about some more of those later.

Ok, lets see some formatting

To scoot the buttons out from the edge we can use margins or padding: margins go on the outside of containers, padding goes on the inside. We can also specify the FontFamily, FontSize, FontWeight, and FontStyle, as well as Foreground and Background colors …


Show-UI {
    StackPanel -Margin 5 -Background Pink {
        Button -Margin 2 "A bed of clams" -FontFamily Consolas -FontSize 24 -FontWeight Bold
        Button -Margin 2 "A coalition of cheetas" -FontFamily Arial -FontSize 20 -FontStyle Italic
        Button -Margin 2 "A gulp of swallows" -FontFamily 'Segoe UI' -FontSize 18 -Foreground Crimson
    }
}
 

So you see, the pink background is on the StackPanel, which has a (default, white) margin around it. If you wanted the whole background of the window to be pink, you would need to set the background of the Window instead of the StackPanel.

An aside on Typography

ShowUI doesn’t need to create a full set of typography-specific top-level elements the way Shoes does, because we are based on WPF, which has a far more powerful typography system available than any we’ve used previously. So, with WPF we start by selecting a control based on how much text you want to put in it, and how much formatting you want to apply: Label and TextBox are the simplest, TextBlock supports limited text formattings, and FlowDocumentViewer or RichTextBox support full rich content. And of course, Hyperlink supports clicking ;)

For the typography elements, the content model changes a little bit. There are basically two types of typographical elements: Inline and Block elements (where inline elements can’t contain block elements). In fact, he TextBlock Content Model is similar to that of an inline element. It is actually a type-restricted “Items” container. Instead of being able to have anything as content, it can only contain Inline flow content elements such as AnchoredBlock, Bold, Hyperlink, InlineUIContainer, Italic, LineBreak, Run, Span, and Underline, and it will create a run automatically if you just put a text string in it.


Show-UI {
   StackPanel -Margin 10 -Children {
      TextBlock "A Question" -FontSize 42 -FontWeight Bold -Foreground "#FF0088"
      TextBlock -FontSize 24 -Inlines {
         Bold "Q. "
         "Are you starting to dig "
         Hyperlink "ShowUI?" -NavigateUri http://huddledmasses.org/tag/showui/ `
                                 -On_RequestNavigate { [Diagnostics.Process]::Start( $this.NavigateUri.ToString() ) }
      }
      TextBlock -FontSize 16 -Inlines {
         Span -FontSize 24 -FontWeight Bold -Inlines "A. "
         "Leave me alone, I'm hacking here!"
      }
}
}
 

Note: If you want support for the full document model (which allows Paragraphs and Lists), you need to use a FlowDocumentReader, FlowDocumentPageViewer, RichTextBox, or a FlowDocumentScrollViewer ... there’s lots more information about those on msdn.

Time for some artwork


Ellipse -Width 60 -Height 80 -Margin "20,10,60,20" -Fill Black -Show
 

In WPF, everything always starts out white, and you must position things based on the container. You can see that the Margin can be specified as a single value as in the previous example, or as separate values for Left, Top, Right, Bottom. Oddly, to satisfy PowerShell’s type-casting, you have to quote them so they’re a single comma-separated string, instead of four separate values.

Some more advanced drawing


Canvas -Height 100 -Width 100 -Children {
   Rectangle -Margin "10,10,0,0" -Width 45 -Height 45 -Stroke "#689945" -StrokeThickness 2 -Fill "#336699"
   Polygon -Stroke Pink -StrokeThickness 2 -Fill DarkRed -Points "10,60", "50,60", "50,50", "65,65",
                                                               "50,80", "50,70", "10,70", "10,60"
} -Show
 

When you want to start getting clever and overlapping things, you need to use a Canvas container. The Canvas can contain multiple items which are all absolutely positioned, but unlike most other containers, it doesn’t automatically expand to contain it’s children, so you typically have to set it’s size.

We also have to set the Stroke and Fill. These are the two colors that make up every object, and again, if we don’t set them, they default to white. Note that you can use named colors, or you can specify a hex value using “#RRGGBB” or “#AARRGGBB” to set the alpha channel. The StrokeThickness controls the line thickness.

One other thing to notice is that we positioned the Rectangle by using the Margin, but we positioned the arrow, which we built using a Polygon, based purely on the x,y coordinates of the points. The available shapes are Ellipse, Line, Path, Polygon, Polyline, and Rectangle. You can, of course, make nearly any shape you want with the Polygon.

There are other more advanced shapes available in external libraries, and we can even do 3D, use gradient or image fills…

We can even get images straight off the web


   Image -Source http://huddledmasses.org/images/PowerBoots/IMG_3298.jpg -MaxWidth 400 -AsJob
 

WPF loads the image on a background thread, and caches it in memory, so the window will show up and be responsive while you’re waiting for the image, and because we’ve specified -AsJob, you can actually continue using PowerShell while the image loads. Note: it will load much faster the second time you run that script. ;)

Events

If you were paying attention to the typography example, you’ll have noticed that we introduced event handling without making a big fuss about it. Event handlers in PowerBoots are specified in much the same way that Properties are, except that their parameter names always start with “On_” and they take a script block. The Hyperlink element in a WPF window doesn’t automatically open a browser (because you can use it to change “pages” in a WPF application), so to make simple web links work, you have to handle the “RequestNavigate” event as shown above.


WrapPanel -ControlName ClickCounter {
    Button "Push Me" -On_Click {
        $ClickCounter.Tag = $ClickCounter.Tag + 1
        $CountLabel.Content = "You clicked the button $($ClickCounter.Tag) times!"
    }
    Label "Nothing pushed so far" -Name CountLabel
} -Show
 

In order to update your user interface when an event triggers, you’ll need to have variables that point at the control(s) you want to affect. In ShowUI event handlers, you get a $this variable which points at the object that caused the event, a $_ variable which is the event arguments, and a $window variable for the top-level window … you also get variables for each named control in your window, which is why I can refer to $ClickCounter and $CountLabel in the Click event handler.

WPF Elements all have a Tag property which can be used to store any object, so as you can see in the previous example, you can use that to keep track of things … but more importantly, the value of the Tag on the top-level control will be output. In other words, when the window is closed, this example actually outputs to the PowerShell pipeline how many times you clicked the button.

Working with User Input

There are helper functions in ShowUI for working with that output value: Set-UIValue and Get-UIValue, and Get-UIValue is particularly helpful because if the control it’s called on has no value, it collects the values of all the child controls, so you can use it to output a whole form at once:

I’ve made this example more complicated than it needed to be to demonstrate some best practices. We could have made it much simpler by using a UniformGrid control instead of a GridControl, thus avoiding needing to set the -Row and -Column special parameters, but I wanted to show those to you anyway, and the form looks a lot better when the two columns can have different sizes:


Grid -ControlName 'Your Information' -Columns Auto,* -Rows 7 -MinHeight 200 -MinWidth 250 {
    Label "First Name"
    New-TextBox -Name First -Column 1
   
    Label "Last Name" -Row 1
    New-TextBox -Name Last -Row 1 -Column 1

    Label "Address" -Row 2
    New-TextBox -Name Address -Row 2 -Column 1

    Label "City" -Row 3
    New-TextBox -Name City -Row 3 -Column 1

    Label "State" -Row 4
    New-TextBox -Name State -Row 4 -Column 1

    Label "Zip" -Row 5
    New-TextBox -Name Zip -Row 5 -Column 1

   
    New-Button "OK" -IsDefault -On_Click {
        Get-ParentControl | Set-UIValue -PassThru | Close-Control
    } -Row 6 -Column 1
} -Show -On_Loaded { $First.Focus() }
 

You’ll notice how easily I specified the width of the columns, but the COUNT of the rows in the Grid panel. If you provide an array of values, they’re converted to GridLengths, but if you provide just one, it’s treated as the count. You can make grid columnds and rows AUTOmatically size to their contents, and you can make them * width to make them take up any extra space. You can even split the extra space by setting * on more than one column (and specify the proportion by using numbers, like: 1*, 2*).

I should also point out that if you use MinHeight and MinWidth instead of the Height and Width values, your controls will be able to size up to fill space when the window is resized! Try that script with Width and Height instead and resize the window to see the difference.

We used the Set-UIValue shortcut in this form, which brings up another point: when creating data forms, you only need to name the controls you want output from. Then you’ll be able to just call Set-UIValue on the parent to collect all the values from the controls and output them as a hashtable!

Finally, remember your defaults: set the focus to something so the user doesn’t HAVE to click to get started, and set a button to -IsDefault with a On_Click handler so that when the user hits enter they can submit the form.

Further directions

There’s a lot more possible with ShowUI: we can use gradients for colors, create data templates and styles, and even make chromeless windows, but you have the basics for getting started already.

This first release of ShowUI is still missing some features from both WPK and PowerBoots, but we’ll get to them as we progress. For now we’d like to invite you to come download the latest release, and write up or vote up the features that you want the most in the next version. Keep an eye on the release page and on the discussions and we’ll be cranking out new releases on a monthly basis for now.

I hope you’ve enjoyed this tour through ShowUI, and will be able to start applying it soon for fun and profit.

1 The -STA switch is necessary because the .Net framework requires the STA threading model in order to do graphical user interfaces. PowerShell ISE doesn’t require it because unlike the PowerShell.exe command console (which is a Win32 native “console” application that hosts the .Net framework), PowerShell ISE is a .Net framework “graphical” application, and is therefore running in the STA model already.