Home > Software engineering >  Powershell Winforms - How to sort Listview by column when items grouped
Powershell Winforms - How to sort Listview by column when items grouped

Time:11-15

Consider the below. The SortListView function works perfectly if the ListView contains ungrouped items.

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object System.Windows.Forms.Form
$form.Text = "Test"
$form.Size = '500,375'
$form.StartPosition = 'CenterScreen'
$form.MaximizeBox = $false
# List of sessions
$LogonList = New-Object System.Windows.Forms.ListView
$LogonList.View = 'Details'
$LogonList.Location = '10,20'
$LogonList.Size = '465,305'
$LogonList.FullRowSelect = $true
$LogonList.Columns.Add('UID',80) | Out-Null
$LogonList.Columns.Add('IPAddress',100) | Out-Null
$LogonList.Columns.Add('HostName',160) | Out-Null
$LogonList.Add_ColumnClick({SortListView $this $_.Column})

$form.Controls.Add($LogonList)
#
Function SortListView {
    Param(
        [System.Windows.Forms.ListView]$xsender,
        $column
    )
    $Script:SortingDescending = !$Script:SortingDescending
    $xsender.Sorting = 'none'
    If (!$xsender.Groups) {
        #
        # No groups in ListView - sort whole list by clicked column property
        #
        $xsender.ShowGroups = $false
        $temp = $xsender.Items | Foreach-Object { $_ }
        $xsender.Items.Clear()
        $xsender.Items.AddRange(($temp | Sort-Object -Descending:$script:SortingDescending -Property @{ Expression={ $_.SubItems[$column].Text } }))
    }
    Else {
        #
        # ListView is grouped, sort each group by clicked column property.
        #
        $xsender.ShowGroups = $true
        $temp = $xsender.Items | Foreach-Object { $_ }
        Write-Host "ListView groups:"
        $temp | Group-Object -Property Group | ForEach-Object {
            Write-Host $_.Name
        }
        $xsender.Items.Clear() 
    }
}
#
$TestData = New-Object System.Collections.ArrayList
$TestData.Add([pscustomobject]@{UID='userjoe';IPAddress='192.168.150.14';Hostname='Workstation99'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userjoe';IPAddress='192.168.150.15';Hostname='Workstation100'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userjoe';IPAddress='192.168.150.16';Hostname='Workstation101'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userjoe';IPAddress='192.168.150.17';Hostname='Workstation102'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userdave';IPAddress='192.168.150.13';Hostname='Workstation104'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userdave';IPAddress='192.168.150.12';Hostname='Workstation105'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userdave';IPAddress='192.168.150.11';Hostname='Workstation106'}) | Out-Null
$TestData.Add([pscustomobject]@{UID='userdave';IPAddress='192.168.150.10';Hostname='Workstation107'}) | Out-Null
#
$TestData | Group-Object -Property UID | ForEach-Object {
    $ThisEntry = New-Object System.Windows.Forms.ListViewGroup
    $ThisEntry.Header = $_.Name
    $LogonList.Groups.Add($ThisEntry) |Out-Null
    $DuplicateUIDs = $_.Group | Sort-Object {$_.IPAddress} -Descending 
    $DuplicateUIDs | ForEach-Object {
        $Entry = New-Object System.Windows.Forms.ListViewItem('-') -ErrorAction Stop
        $Entry.SubItems.Add([string]$_.IPAddress) | Out-Null
        $Entry.SubItems.Add([string]$_.HostName) | Out-Null
        $Entry.Group = $ThisEntry
        # Add compiled object to ListView box
        $LogonList.Items.Add($Entry) | Out-Null
    }
}
$form.Activate()
$form.ShowDialog() | Out-Null
$form.Dispose()

I'd like to code for the same behaviour when the items are grouped - in the above example clicking the column header should order all the group items within the groups by that column. I'm really struggling to get my head round the different objects/item assignments and come up with a sensible solution - any help appreciated.

CodePudding user response:

if you are using PowerShell 5 or above, you can achieve ListView sort in an easier and faster way :

First, implement a Comparer Interface somewhere in your code (you can add it right before your sort function) :

class ListViewItemComparer : System.Collections.IComparer
{
    [int]$col
    [System.Windows.Forms.SortOrder]$order

    ListViewItemComparer()
    {
         $this.col = 0
         $this.order = [System.Windows.Forms.SortOrder]::Ascending
    }
    ListViewItemComparer([int]$column, [System.Windows.Forms.SortOrder]$sortOrder)
    {
         $this.col = $column
         $this.order = $sortOrder
    }
    [int]Compare([object]$x, [object]$y)
    {
         $result = [String]::Compare( `
                   ([System.Windows.Forms.ListViewItem]$x).SubItems[$this.col].Text,`
                   ([System.Windows.Forms.ListViewItem]$y).SubItems[$this.col].Text);
         if ($this.order -eq [System.Windows.Forms.SortOrder]::Ascending)
         {
              return $result
         }
         else
         {
              return -($result)
         }
    }
}

then your sort function will be as simple by setting the ListViewItemSorter Property to a new comparer interface :

Function SortListView {
     Param(
          [System.Windows.Forms.ListView]$xsender,
          $column
     )
     $Script:SortingDescending = !$Script:SortingDescending
     if ($Script:SortingDescending)
     {
          $xsender.Sorting = [System.Windows.Forms.SortOrder]::Descending
     }
     else
     {
          $xsender.Sorting = [System.Windows.Forms.SortOrder]::Ascending
     }
     $xsender.ListViewItemSorter = [ListViewItemComparer]::new($column, $xsender.Sorting)
}

This works whatever there is a group or not.

some more information here and here

EDIT :

Do not forget this line before creating your form

Add-Type -AssemblyName System.Windows.Forms

If you are using an old version of PowerShell, implement the compare interface with C# this way :

$compareCode = @"
using System;
using System.Collections;
using System.Windows.Forms;

namespace Tools
{
    public class ListViewItemComparer : IComparer
    {
        private int col;
        private SortOrder order;
        public ListViewItemComparer()
        {
            col = 0;
            order = SortOrder.Ascending;
        }
        public ListViewItemComparer(int column, SortOrder sortOrder)
        {
            col = column;
            order = sortOrder;
        }
        public int Compare(object x, object y)
        {
            int result = String.Compare(
                         ((ListViewItem)x).SubItems[col].Text,
                         ((ListViewItem)y).SubItems[col].Text);
            if (order == SortOrder.Ascending)
            {
                return result;
            }
            else
            {
                return -result;
            }
        }
    }
}
"@

Add-Type -TypeDefinition $compareCode -ReferencedAssemblies System.Windows.Forms

then call the interface in your sort function (change only this line) :

$xsender.ListViewItemSorter = [Tools.ListViewItemComparer]::new($column, $xsender.Sorting)

EDIT 2

The PowerShell class option needs to call Add-Type -AssemblyName System.Windows.Forms BEFORE you run the script in order to work correctly. This is due to the way PowerShell is parsing code before execution.

#Call script
Add-Type -AssemblyName System.Windows.Forms
.\MyFormScriptWithICompareClass.ps1

The C# version does not have this issue.

  • Related