After posting my last post, I started thinking that perhaps I shouldn’t really have started with something so splashy [rolleyes]. So I started thinking about what I could use as a proper example — not of what WPF can do, but of what you might want to use WPF for in PowerShell.

I came up with two really obvious ideas which I think are both good demonstrations of useful things to do from PowerShell, and are good examples of something that really just doesn’t work with WinForms.

User Input prompts

I’ll write more about this in future posts, but for now, take as an ultimate example the latest post from mow, The PowerShell Guy in which he rewrites his PowerShell WMI Explorer to be WPF based — it seems to not only run faster, but be based on less code (of course, that might just be because he’s gained experience since the first version).

Data-bound Graphs

A WPF databound bar graph dialog from PowerShellThere are dozens of ways you can do data-bound graphs in PowerShell — including 3d — but the simplest to demonstrate (and coincidentally the easiest for me to come up with a use-case for) is a plain bar graph. I’ve written a simple graph window in XAML, and created a script Out-BarGraph which lets you send data to it … but there are, as the Genie says, some caveats, provisos, addendums, and quid pro quos. ;)

The main catch is that you have to specify the ScriptBlock parameters so that the script can figure out which properties to use as the Labels and Values — the purpose of this script is not to provide the most efficient way to bind PowerShell data to WPF, but rather to bind the data in a way that is easily translatable to new data types — because of this, you can use the script with file sizes, web traffic logs, performance monitor data, etc.

The second catch is that we export the data to a temp file. In order to make it easy to handle multiple data-types, the WPF actually databinds to XML which we create by using PowerShell’s built in Export-CliXml cmdlet. The only constraint that whatever is exported must have three specific properties (Label, Value, Percentage) which the Out-GraphableXml function in the script calculates using script blocks you provide. I chose to do this so that you could see that you could create the xml from anywhere easily … but you could choose to databind to actual objects instead.

Finally, there are a few additional constraints. This bar graph doesn’t update itself — it’s just a static graph of a given set of data. It can’t represent negative numbers. You must provide the percentage attribute as a number between 0 and 1 (again, in my example script the Out-GraphableXml function in the script calculates the percent based on the largest value).

The Script

Having laid out the shortcomings of the script, let me present it to you here in full glory. Unlike the downloadable version linked above, this script includes the XAML source in-line as a Data block, so the entire script and UI code are in a single file. Scroll to the end of the script for some explanations…


#requires -version 2
## Out-BarGraph.ps1
## A script to generate WPF bargraphs using XAML
## REQUIRES graph.xaml ( included here as a DATA section )
########################################################################################################################
## Example Usage
## ls | Out-BarGraph "File Sizes in Bytes" -Label {$_.Name} -Value {$_.Length}
########################################################################################################################
## Version History
## 1.4   - Embedded XAML in script
## 1.3   - Added [scriptblock] parameters, removing need for data to be a custom psobject
## 1.2   - Wrapper script now sets all values
## 1.1.3 - Added data-bound "scale" labels
## 1.1.2 - Added data-bound caption
## 1.1.1 - Added data-bound labels
## 1.1   - Got data-binding to the graph working
## 1.0   - First working version, no live data binding
########################################################################################################################

Param(
   $caption= "A bar-graph from WPF",
   $data = @(),
   [scriptblock]$Label = $(Throw "You must specify the 'Label' scriptblock"),
   [scriptblock]$Value = $(Throw "You must specify the 'Value' scriptblock"),
   [switch]$sideways
)
BEGIN {
DATA XAML {
@"<Window  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:system="clr-namespace:System;assembly=mscorlib" WindowStartupLocation='CenterScreen'
         WindowStyle="ToolWindow" ShowInTaskbar='True' SizeToContent='Width' Height="400" Width="600">
   <Window.Resources>
      <system:String x:Key="GraphCaption">-</system:String>
      <RotateTransform x:Key="Vertical" Angle="-90" />
      <RotateTransform x:Key="Sideways" Angle="0" />
      <XmlNamespaceMappingCollection x:Key="PowerShellMappings">
         <XmlNamespaceMapping Uri="http://schemas.microsoft.com/powershell/2004/04" Prefix="ps" />
      </XmlNamespaceMappingCollection>
      <XmlDataProvider x:Key="Data" XPath="/ps:Objs/ps:Obj" XmlNamespaceManager="{StaticResource PowerShellMappings}"
         Source="C:\Users\JBennett\Documents\WindowsPowerShell\Scripts\Demo\data.xml" />
      <XmlDataProvider x:Key="scale" XPath="/scale" />
      <DataTemplate x:Key="SeriesLabels">
         <Label Height="28" Margin="5,0,0,0" HorizontalContentAlignment="Right" LayoutTransform="{StaticResource Vertical}" Content="{Binding  XPath=*/*[@N\=\'Label\']}" />
      </DataTemplate>
      <DataTemplate x:Key="bars">
         <Rectangle Width="28" Margin="5,0,0,0" Fill="Red" RenderTransformOrigin="0,1" ToolTip="{Binding XPath=*/*[@N\=\'Value\']}">
            <Rectangle.RenderTransform>
               <ScaleTransform x:Name="Scaler" ScaleX="1" ScaleY="{Binding XPath=*/*[@N\=\'Percent\']}" />
            </Rectangle.RenderTransform>
         </Rectangle>
      </DataTemplate>
   </Window.Resources>
<DockPanel Margin="10">
<Label DockPanel.Dock="Top" HorizontalAlignment="Center" FontFamily="Garamond" FontSize="20"
       Content="{DynamicResource GraphCaption}" />
      <Grid LayoutTransform="{DynamicResource Sideways}" Name="Graph" ShowGridLines="True" >
         <Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
         <Grid.RowDefinitions><RowDefinition Height="10*"/><RowDefinition Height="10*"/><RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/><RowDefinition Height="10*"/><RowDefinition Height="10*"/><RowDefinition Height="10*"/>
            <RowDefinition Height="10*"/><RowDefinition Height="10*"/><RowDefinition Height="10*"/><RowDefinition Height="Auto"/>
         </Grid.RowDefinitions>
         <Label Grid.Row="0" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[1]/@Label}" />
         <Label Grid.Row="1" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[2]/@Label}" />
         <Label Grid.Row="2" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[3]/@Label}" />
         <Label Grid.Row="3" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[4]/@Label}" />
         <Label Grid.Row="4" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[5]/@Label}" />
         <Label Grid.Row="5" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[6]/@Label}" />
         <Label Grid.Row="6" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[7]/@Label}" />
         <Label Grid.Row="7" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[8]/@Label}" />
         <Label Grid.Row="8" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[9]/@Label}" />
         <Label Grid.Row="9" Padding="0,0,4,2" Content="{Binding Source={StaticResource scale}, XPath=//Mark[10]/@Label}"/>
         <ItemsControl Name="Bars" Grid.RowSpan="10" Grid.Column="1" Background="#FFD7F39F"
                       ItemsSource="{Binding Source={StaticResource Data}}" ItemTemplate="{StaticResource bars}" >
         <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
               <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
         </ItemsControl.ItemsPanel>
      </ItemsControl>
         <ItemsControl Name="SeriesLabels" Grid.Column="1" Grid.Row="11" Margin="0,0,0,0"
         ItemsSource="{Binding Source={StaticResource Data}}" ItemTemplate="{StaticResource SeriesLabels}" >
            <ItemsControl.ItemsPanel>
               <ItemsPanelTemplate>
                  <StackPanel Orientation="Horizontal"/>
               </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
         </ItemsControl>
      </Grid>
   </DockPanel>
</Window>
"@

}
   function Out-GraphableXml ($data, [ref]$max, [scriptblock]$Label, [scriptblock]$Value) {
      ### Get a "Max" number which should be the next big round number (eg: 5,10,50,100,150 ...)
      $max.Value = ($data | Sort-Object {&$Value -as [double]} -Desc | Select-Object @{n="Value";e=$Value} -First 1).Value -as [double]
      if( $max.Value -gt 1 ) {
         $siz = [Math]::Pow( 10, [Math]::Truncate($max.Value).ToString().Length -1 ) / 2
      } else {
         $siz = (.5 / (10 * ($max.Value.ToString().Length - 2 - $max.Value.ToString().TrimStart("0.").Length)))
      }
      $max.Value = [Math]::Ceiling( $max.Value / $siz ) * $siz

      ## Export the data ...
      $path = [IO.Path]::GetTempFileName()
      $data | Select-Object @{n="Value";e=$Value},@{n="Label";e=$Label},@{n="Percent";e={($_ | &$Value)/$max.Value}} | export-clixml $path
      return $path
   }
}

PROCESS {
   if($_) {
      $data += $_
   }
}
END {
   Write-Host "Loading XAML window..." -fore Cyan
   
   ### Import the WPF assemblies
   Add-Type -Assembly PresentationFramework
   Add-Type -Assembly PresentationCore
   ## I've removed the xaml to a separate document because it's getting too big for my example :)
   $graph = [Windows.Markup.XamlReader]::Load( (New-Object System.Xml.XmlNodeReader ([Xml]$XAML)) )
   $graph.Resources.Data.Source = Out-GraphableXml $data ([ref]$max) $Label $Value

   ### Generate 10 labels ...
   $graph.Resources.scale.Document = &{
      "<scale xmlns=''>"
   10..1 | ForEach-Object { "<Mark Label='$(($max/10)*$_)' />" }
      "</scale>"
   }
   ### And set the caption
   $graph.Resources.GraphCaption = $caption
   
   ### Maybe rotate the whole thing
   if($sideways) {
      $graph.Resources.Sideways.Angle = 90
   }

   $graph.ShowDialog()
   # Don't forget to delete that temp file...
   Remove-Item $graph.Resources.Data.Source.AbsolutePath
}
 

Some XAML Tips

I learned a few things about XAML markup while doing this.

  1. If you bind to an XML data source that’s in-line in your XAML, you do so using the x:XData element, and then a single root element inside that. You must specify a blank xmlns parameter on that root element, or it won’t work. However, when you bind to an eternal XML document, as in our case — you need to specify the XML namespaces using a XmlNamespaceMapping collection — and you must use a prefix, and then repeat that prefix throughout your Xaml whenever you specify an XPath. However, attributes don’t require the prefix (I’m not actually sure why) so in the code above I simply used wildcards for practically every element.
  2. When you specify attribute tests (like /Node[attribute='value']) in binding markup, you get an error because of the equals sign and quotes, but you can just escape them with a backslash, or you can use a Binding@ element and put the test in the XPath attribute (which doesn’t need escaping).
  3. The XAML Grid element is the only element that supports proportional sizing and supports making all the things in a column (or row) the size of the largest item. It’s a pain to work with by hand because you have to specify each RowDefinition and ColumnDefinition and then assign each child control a Grid.Column and Grid.Row and possibly even a Grid.ColumnSpan or Grid.RowSpan
  4. If you only learn two of the “complicated” controls, learn about Grid and ItemsControl.

Some PowerShell WPF Tips

  1. You can generate XML using XDocument instead of XmlDocument, but to be perfectly honest, since you’re already moving slow, the simplest way to write XML in PowerShell is to just use here-strings. In fact, if you need to generate a bunch of rows of XML, this snippet should help send you in the right direction:
    [XML]$xml = &{
    "<?xml version='1.0' encoding='UTF-8'?>"
    "<scale xmlns=''>"
    10..1 | ForEach-Object { "<Mark Label='$(($max/10)*$_)' />" }
    "</scale>"
    }
     
  2. Although the about_data_section help says that you can use [XML]”...” as a literal in a Data section — you can’t. Just get used to casting it on the way out … Also,
  3. If you need to cast a parameter to a function, (or specify it as a reference parameter) you have to enclose it in parenthesis: Out-GraphableXml $data ([ref]$max) $Label $Value — it’s not enough to put the type there.
  4. Don’t forget, reference parameters in PowerShell are actually a type (PSReference), and to access the variable inside a PSReference variable $foo, you have to use $foo.Value.
  5. Sometimes, the Dispatcher.Invoke trick isn’t enough — in the case of this data-bound form, it needs to go through at least 3 cycles of handling events before it will finish drawing:
    $graph.Show()
    1..3 | %{ $graph.Dispatcher.Invoke( "Render", [Windows.Input.InputEventHandler]{ }, $null, $null) }