WPF From PowerShell – Select-Grid

After looking over the scripts I’ve pasted in the last few days it struck me that all of them load the UI from XAML — and for the most part, you can do pretty much whatever you need to do in WPF in pure PowerShell if you want to.

Select-Grid ScreenshotTo demonstrate this, I wrote a simple Out-Grid script. But then I got carried away, and wrote the script I’ve shown in this screenshot. :-D The resulting script not only illustrates how to code WPF in PowerShell without XAML, it also illustrates one of the few areas where you’re much better off loading from XAML than writing the plain code — and that’s a DataTemplate. It is possible to write them in code, but believe it or not, some of their features are accessible only from XAML.

[new] I’ve updated both the script and the screenshot to correct a bug I noticed last night after I posted this … so if you tried this and got a lot more columns than you had bargained for … here is the Select-Grid script again. [new] I updated it again just now to switch my Set-PsDebug statement for a Set-StrictMode statement, after reading Jeffrey Snover’s post about strict mode best practices.

I’m not going to paste the whole script in-line, but I thought I’d paste some of it here so that I could give a little explanation of what’s happening, in case you need a little help figuring it out…

Using modules …

Some of you may know that normally in a script like this we would put the “utility” functions inside the BEGIN block so-as to encapsulate them from your main runspace, and may be wondering why I didn’t in this case. It’s because the new CTP2 of PowerShell has this concept called a “module” ... which is somewhat like a script snapin — basically it allows you to have functions in the script which don’t end up in the main runspace, but which are still available to the other functions (or cmdlets, in this case) in the script.

I will write a separate post about how PowerShell Modules work, and what you can do with them … it’s too complicated to get into here. Suffice it to say that all I had to do to the script is add a line at the bottom: Export-ModuleMember Select-Grid and rename it to [new] Select-Grid.psm1 and put it in a specific location: Documents\WindowsPowerShell\Packages\Select-Grid\Select-Grid.psm1 — but of course, I couldn’t resist cleaning it up a bit as I was writing about it (if you grabbed it before v3.4, you should grab it again — look in the comment at the top).

Having done that, all you have to do is run Add-Module Select-Grid instead of dot-sourcing it, and then run it as before (see the screenshot).

Let’s talk about code bay-bee …

There’s a few things I wanted to point out in the module’s code, so lets get to that. The first thing is that the main code is in a cmdlet, rather than a function — mostly because the cmdlet automatically handles both command arguments and pipeline arguments and allows me to handle them both the same way. Since I want to handle all of the input at once, my BEGIN and PROCESS blocks are basically empty, serving only to collect the inputs.

One caveat I will point out: The CTP has a problem where it strips the ETS attributes from things when you specify the type. So if you write a cmdlet with a parameter types like [System.IO.FileSystemInfo], and then add data to them with Add-Member, that data won’t show up in the script cmdlet. Hopefully this will be considered an important bug and will be fixed in the next CTP/Beta. The workaround in the meantime is to just use [PSObject] when you want to get the ETS attributes.

So, lets review the important bits of this …


      $window = New-Object System.Windows.Window
      $window.SizeToContent = "WidthAndHeight"
      $window.SnapsToDevicePixels = $true
      $window.Content = New-Object System.Windows.Controls.ListView
      if($Title) {
         $window.Title = $Title
      } else {
         $window.Title = $outputObjects[-1].GetType().Name
      }
      ### The ListView takes ViewBase object which controls the layout and appearance
      ### We'll use a GridView
      $window.Content.View = New-Object System.Windows.Controls.GridView
      $window.Content.View.AllowsColumnReorder = $true

This constructs the window, and the only things I’ll point out here is that I used SizeToContent so that the window would automatically adjust it’s size, and I put the ListView and GridView directly into their containers, instead of assigning them to variables first. It might help you to have a $grid or a $list variable to work with in the rest of the code (eg: instead of $window.Content.View = new...GridView, you’d be doing $list.View = $grid), but since this is PowerShell and not C#, I don’t end up having to manually cast things to use their specific properties, so this actually keeps my code cleaner. In C# this would be a mess, because the View property is only a property of a ListView type, so you would have to cast the $Window.Content to a ListView in order to use that property, and then … well, lets just say the last line of that code would end up something like this (assuming you were using the namespaces and didn’t have to type them out):

((GridView)((ListView)window.Content).View).AllowsColumnReorder = true

The next interesting feature of this code (at least, in my opinion) is the Get-PropertyTypes function — I won’t elaborate much here, but the purpose of this code is to get a hash of the property names and types when the user specifies a list of properties to show. The main reason I did this that I was having problems sorting columns with missing values, so I go through the values and add a NoteProperty with an appropriate default (for which I need to know the variable’s type).

We then construct columns for each property we want to bind to. This could be drastically simplified if we didn’t need the sorting, but since we do, it looks like this:


      foreach($Name in $Properties) {
         ## For each property, make a column        
         $gvc = New-Object System.Windows.Controls.GridViewColumn
         ## And bind the data ...
         $gvc.DisplayMemberBinding = New-Object System.Windows.Data.Binding $Name
         ## In order to add sorting, we need to create the header ourselves
         $gvc.Header = New-Object System.Windows.Controls.GridViewColumnHeader
         $gvc.Header.Content = $Name
         ##
         ## Add a click handler to enable sorting ...
         $gvc.Header.add_click({
            $view = [System.Windows.Data.CollectionViewSource]::GetDefaultView( $outputObjects )
            $view.SortDescriptions.Clear()
            $view.SortDescriptions.Add( (New-SortDescription $this.Content) )
            $view.Refresh()
         } )
         ## Use that column in the view
         $window.Content.View.Columns.Add($gvc)
      }
 

That block only has one thing worth mentioning that might not be clear from the comments: to avoid double-sorting the data, we have to Clear() the SortDescriptions before we add a new one (otherwise it sorts when we add, and then sorts again when we remove the old SortDescription). Since we need to know the direction that the current sort is in, I refactored it into a module-level variable, and wrote a New-SortDescription function which creates the SortDescription based on that … allowing me to clear without first capturing the current sort direction, and then add the SortDescription straight to the GridView. Script modules rock — they are essentially a “class” semantic for PowerShell, with private member variables and everything.

The last thing I should write about is the CellTemplate magic. If you pass a Graph parameter to the cmdlet, it puts a custom CellTemplate on each column you name. If you specified the Properties parameter, you have to be careful to only specify column names which you also included there, and in general, they have to be numeric columns in order for this to work.


      foreach($obj in $outputObjects) {
         Add-Member NoteProperty "$($property)Percent" (
                  ($($obj.$($property)) -as [double]) / $($max.($property))) -input $obj
      }

      $column = @($gridview.Columns | ? { $_.Header.Content -eq $property })[0];
      ## dump the binding and use a template instead... (this shouldn't be necessary)...
      $column.DisplayMemberBinding = $null
      $column.CellTemplate = `
      [Windows.Markup.XamlReader]::Load(
         (New-Object System.Xml.XmlNodeReader (
         [Xml]"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
                  <Grid>
                     <Rectangle Margin='-6,0' VerticalAlignment='Stretch' RenderTransformOrigin='0,1' >
                        <Rectangle.Fill>
                           <LinearGradientBrush StartPoint='0,0' EndPoint='1,0'>
                              <GradientStop Color='#FFFF4500' Offset='0' />
                              <GradientStop Color='#FFFF8585' Offset='1' />
                           </LinearGradientBrush>
                        </Rectangle.Fill>
                        <Rectangle.RenderTransform>
                           <ScaleTransform ScaleX='{Binding $($property)Percent}' ScaleY='1' />
                        </Rectangle.RenderTransform>              
                     </Rectangle>              
                     <TextBlock Width='100' Margin='-6,0' TextAlignment='Right' Text='{Binding $property}' />
                  </Grid>
               </DataTemplate>"
)))

There’s really only three lines of code here (not counting the Xaml), so lets review them. First we add a “PropertyPercent” NoteProperty to each output object for each “Property” that we want to graph, and populate it with the appropriate value. Then, we find the appropriate column in the GridView that we’ve already set up — and remove it’s DisplayMemberBinding. If we don’t remove the binding, the CellTemplate has no effect.

Finally, we add the CellTemplate. The magic is that the cell template has a colored Rectangle behind a TextBlock, and uses a ScaleTransform — bound to the “PropertyPercent” NoteProperty we created earlier — to scale the width of the Rectangle. I can’t stress enough that this is just one of hundreds of possible visualizations you could use, so let me give you a few ideas, and then you should go find a friend that’s an artist or a designer and get them to spend some time playing with Expression Blend …

  1. You could have a DataTransform that converted numbers to colors, and bind the TextBlock’s BackgroundBrush to that so each cell is simply colored differently (eg: to color cells from red to blue based on a temperature value).
  2. You could databind the FontSize of a TextBlock based on a numeric value in a non-displayed property, so you could show words with relative size (like a “Tag Cloud”).
  1. You could databind the Angle property of the RotateTransform on a pair of lines or shapes to create an analog clock to display times

So many possibilities …

Similar Posts:

6 thoughts on “WPF From PowerShell – Select-Grid”

  1. I like this promising script and the idea itself. I have a warning though:

    The script invokes Set-PSDebug -Strict. It is nice for development, indeed, but it should be avoided in production scripts (or, say, published scripts) because this command changes session state globally and quite seriously – some other commands or scripts may fail after using this script (even after dot-sourcing only) with typical error message “The variable … cannot be retrieved because it has not been set yet.”


    Thanks,
    Roman Kuzmin

  2. Very nice progress, really!

    Strict mode seems to be too strict now :) . This command:

    ls | Select-Grid

    triggers an error in function Get-DefaultValue at line “if( $type.IsValueType) {“ (line 135). “IsValueType” is not found. When I turn strict mode off then it works, but this command has yet another effect: right part of the grid window is out of the screen; is this OK?


    Thanks,
    Roman Kuzmin

Comments are closed.