Eric's Technical Outlet

Learning the hard way so you don't have to

PowerShell: Sort a Windows Forms ListView without a Custom Comparer

I realize that PowerShell is mainly for command-line operations and scripting, but I’ve found more than a few uses to have it present a GUI. From my own portfolio, CoreFig would be a prime example. What GUIs are especially good for is presentation, selection, and manipulation of complex data structures. One control that’s suited for such uses is the ListView. Using a ListView in PowerShell isn’t more difficult than using most any other Windows Forms object. Sorting it, on the other hand, isn’t simple. Most of the solutions I found did not work at all, only did a sort-of sort, or did some complicated work with the .Net Framework. I have concocted a solution that stays in PowerShell.

Paste the following into an empty .ps1 file and execute for a demo. Please be aware that not all PowerShell hosts are created equally, and this script will generate errors in some. It is not an issue with the script, but with the host. Try running it from a standard PS environment or ISE.

The real work is done in the SortListView function. Pay attention to the event handler as well.

<#
.SYNOPSIS
    Demonstration of System.Windows.Forms.ListView sorting via PowerShell.
.DESCRIPTION
    Displays a basic Windows Form with a ListView control and some items. Each column is sortable.
.LINK
    https://etechgoodness.wordpress.com/2014/02/25/sort-a-windows-forms-listview-in-powershell-without-a-custom-comparer/
.NOTES
    By Eric Siron
    Version 1.0.1 June 6th 2014:  Slightly modified to work with a wider range of PS hosts and versions.
    Version 1.1 August 12th 2015: Improved test procedure per feedback from reader Stanley
#>

## Set up the environment
Add-Type -AssemblyName System.Windows.Forms
$LastColumnClicked = 0 # tracks the last column number that was clicked
$LastColumnAscending = $false # tracks the direction of the last sort of this column

## Create a form and a ListView
$Form = New-Object System.Windows.Forms.Form
$ListView = New-Object System.Windows.Forms.ListView

## Configure the form
$Form.Text = "ListView Sort Demo"

## Configure the ListView
$ListView.View = [System.Windows.Forms.View]::Details
$ListView.Width = $Form.ClientRectangle.Width
$ListView.Height = $Form.ClientRectangle.Height
$ListView.Anchor = "Top, Left, Right, Bottom"

# Add the ListView to the Form
$Form.Controls.Add($ListView)

# Add columns to the ListView
$ListView.Columns.Add("Item Name", -2) | Out-Null
$ListView.Columns.Add("Color") | Out-Null
$ListView.Columns.Add("Size") | Out-Null
$ListView.Columns.Add("Weight") | Out-Null

# Add list items
$ListViewItem = New-Object System.Windows.Forms.ListViewItem("Barlav")
$ListViewItem.Subitems.Add("Green") | Out-Null
$ListViewItem.Subitems.Add("Tiny") | Out-Null
$ListViewItem.Subitems.Add("11") | Out-Null
$ListView.Items.Add($ListViewItem) | Out-Null

$ListViewItem = New-Object System.Windows.Forms.ListViewItem("Floomquet")
$ListViewItem.Subitems.Add("Red") | Out-Null
$ListViewItem.Subitems.Add("Large") | Out-Null
$ListViewItem.Subitems.Add("2") | Out-Null
$ListView.Items.Add($ListViewItem) | Out-Null

$ListViewItem = New-Object System.Windows.Forms.ListViewItem("Gardgel")
$ListViewItem.Subitems.Add("Yellow") | Out-Null
$ListViewItem.Subitems.Add("Jumbo") | Out-Null
$ListViewItem.Subitems.Add("7") | Out-Null
$ListView.Items.Add($ListViewItem) | Out-Null

$ListViewItem = New-Object System.Windows.Forms.ListViewItem("Wilbit")
$ListViewItem.Subitems.Add("Turquoise") | Out-Null
$ListViewItem.Subitems.Add("Gigantic") | Out-Null
$ListViewItem.Subitems.Add("1") | Out-Null
$ListView.Items.Add($ListViewItem) | Out-Null

$ListViewItem = New-Object System.Windows.Forms.ListViewItem("Zasitch")
$ListViewItem.Subitems.Add("Beige") | Out-Null
$ListViewItem.Subitems.Add("Small") | Out-Null
$ListViewItem.Subitems.Add("5") | Out-Null
$ListView.Items.Add($ListViewItem) | Out-Null

## Set up the event handler
$ListView.add_ColumnClick({SortListView $_.Column})

## Event handler
function SortListView
{
 param([parameter(Position=0)][UInt32]$Column)

$Numeric = $true # determine how to sort

# if the user clicked the same column that was clicked last time, reverse its sort order. otherwise, reset for normal ascending sort
if($Script:LastColumnClicked -eq $Column)
{
    $Script:LastColumnAscending = -not $Script:LastColumnAscending
}
else
{
    $Script:LastColumnAscending = $true
}
$Script:LastColumnClicked = $Column
$ListItems = @(@(@())) # three-dimensional array; column 1 indexes the other columns, column 2 is the value to be sorted on, and column 3 is the System.Windows.Forms.ListViewItem object

foreach($ListItem in $ListView.Items)
{
    # if all items are numeric, can use a numeric sort
    if($Numeric -ne $false) # nothing can set this back to true, so don't process unnecessarily
    {
        try
        {
            $Test = [Double]$ListItem.SubItems[[int]$Column].Text
        }
        catch
        {
            $Numeric = $false # a non-numeric item was found, so sort will occur as a string
        }
    }
    $ListItems += ,@($ListItem.SubItems[[int]$Column].Text,$ListItem)
}

# create the expression that will be evaluated for sorting
$EvalExpression = {
    if($Numeric)
    { return [Double]$_[0] }
    else
    { return [String]$_[0] }
}

# all information is gathered; perform the sort
$ListItems = $ListItems | Sort-Object -Property @{Expression=$EvalExpression; Ascending=$Script:LastColumnAscending}

## the list is sorted; display it in the listview
$ListView.BeginUpdate()
$ListView.Items.Clear()
foreach($ListItem in $ListItems)
{
    $ListView.Items.Add($ListItem[1])
}
$ListView.EndUpdate()
}

## Show the form
$Response = $Form.ShowDialog()
Advertisements

9 responses to “PowerShell: Sort a Windows Forms ListView without a Custom Comparer

  1. foxmolder147 March 29, 2017 at 4:59 pm

    What about date sorting ?
    format like: gg/MM/yyyy

    Like

    • Eric Siron April 12, 2017 at 10:47 am

      I keep meaning to reply to this but I never find the time to do any work. It could be extended to handle dates as well. What I would probably do is modify the “foreach” that starts at line 94 to detect the date format and then use .Ticks to convert it to a number. Then the sort should work without tinkering with the other items. I don’t have the time right now to flesh that theory out.

      Like

  2. chair6 January 14, 2016 at 11:44 pm

    Hey Eric. I stole your Sort-ListView function for inclusion in a little Active Directory user management GUI I wrote (https://github.com/chair6/madness). Very handy little piece of code, thank you!

    Like

    • chair6 January 15, 2016 at 9:03 am

      PS. Funnily enough, some people want to contribute to this thing. Do you mind if I continue to include your function in https://github.com/chair6/madness if I put a BSD license on it?

      Like

      • Eric Siron January 15, 2016 at 9:07 am

        That would be fine. Thank you for asking.

        Like

  3. Stanley August 12, 2015 at 9:04 am

    $CHANGE: $Test = [Double]$ListItem.SubItems.Text[$Column]
    $ListItems += ,@($ListItem.SubItems.Text[$Column],$ListItem)

    TO: $Test = [Double]$ListItem.SubItems[[int]$Column].Text
    $ListItems += ,@($ListItem.SubItems[[int]$Column].Text,$ListItem)

    and it should work fine

    Liked by 1 person

  4. Tu Pham June 6, 2014 at 10:11 am

    It doesnt even work dude.. fulll of errors:

    Cannot index into a null array.
    At line:94 char:48
    + $ListItems+=,@($ListItem.SubItems.Text[ <<<< $Column], $ListItem)
    + CategoryInfo : InvalidOperation: (3:UInt32) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At line:94 char:48
    + $ListItems+=,@($ListItem.SubItems.Text[ <<<< $Column], $ListItem)
    + CategoryInfo : InvalidOperation: (3:UInt32) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At line:94 char:48
    + $ListItems+=,@($ListItem.SubItems.Text[ <<<< $Column], $ListItem)
    + CategoryInfo : InvalidOperation: (3:UInt32) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At line:94 char:48
    + $ListItems+=,@($ListItem.SubItems.Text[ <<<< $Column], $ListItem)
    + CategoryInfo : InvalidOperation: (3:UInt32) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At line:94 char:48
    + $ListItems+=,@($ListItem.SubItems.Text[ <<<< $Column], $ListItem)
    + CategoryInfo : InvalidOperation: (3:UInt32) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Exception calling "Add" with "1" argument(s): "Object reference not set to an instance of an object."
    At line:110 char:28
    + $ListView.Items.Add <<<< ($ListItem[1])
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Exception calling "Add" with "1" argument(s): "Object reference not set to an instance of an object."
    At line:110 char:28
    + $ListView.Items.Add <<<< ($ListItem[1])
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Exception calling "Add" with "1" argument(s): "Object reference not set to an instance of an object."
    At line:110 char:28
    + $ListView.Items.Add <<<< ($ListItem[1])
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Exception calling "Add" with "1" argument(s): "Object reference not set to an instance of an object."
    At line:110 char:28
    + $ListView.Items.Add <<<< ($ListItem[1])
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Exception calling "Add" with "1" argument(s): "Object reference not set to an instance of an object."
    At line:110 char:28
    + $ListView.Items.Add <<<< ($ListItem[1])
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Like

    • Eric Siron June 6, 2014 at 1:17 pm

      I found a small bug which, for some reason, didn’t manifest in all PowerShell hosts. Try now.

      Like

      • Eric Siron June 6, 2014 at 1:47 pm

        On further testing, I had it right the first time. There is just something wrong with the way that some PowerShell hosts process Windows Forms objects. This has to be run from an actual PS or the ISE prompt to guarantee results as expected. Sorry

        Like

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: