The “Spotlight on Controls” series focuses on a single WinForms control in PrimalForms 2011 , details the important Properties, Methods, and Events of the control and demonstrates how to utilize the control. Most of the information about the controls is still applicable to previous versions of PrimalForms.
In Part 2 of the Spotlight on the ListView Control, we covered the control’s properties as well as most of the methods and events. In part 3 we will cover how you can sort items and utilize various helper functions.
Sorting ListViewItems:
The ListView supports basic sorting, but it only handles sorting by the first column. In case you want to sort using the content of other columns, you need to write some additional code. Typically you want to sort when a user clicks on a column header, so we will be using the ColumnClick event to trigger sorting.
Simple Solution:
The simplest way to change the sort items is to toggle the Sort property:
$listview1_ColumnClick=[System.Windows.Forms.ColumnClickEventHandler]{ #Event Argument: $_ = [System.Windows.Forms.ColumnClickEventArgs] if($this.Sorting -eq'Ascending') { $this.Sorting ='Descending' } }
The problem with this simplistic approach is that it doesn’t take into account which column was clicked. In this case, the Sorting property just doesn’t cut it, unless you only have one column.
Possible Alternative Solution:
Next you can try to create you own sort procedure in PowerShell depending on the column clicked.
You can manually sort the items by doing the following:
$listview1_ColumnClick=[System.Windows.Forms.ColumnClickEventHandler]{ #Event Argument: $_ = [System.Windows.Forms.ColumnClickEventArgs] $array = New-Object System.Collections.ArrayList foreach($item in $listview1.Items) { $psobject = New-Object PSObject $psobject | Add-Member -type 'NoteProperty' -Name 'Text'` -value $item.SubItems[$_.Column].Text $psobject | Add-Member -type 'NoteProperty' -Name 'ListItem'` -value $item $array.Add($psobject) } $array = ($array | Sort-Object -Property Text) $listview1.BeginUpdate() $listview1.Items.Clear(); foreach($item in $array) { $listview1.Items.Add($item.ListItem) } $listview1.EndUpdate(); }
Note: When you manually sorting the List, you will want to set the Sorting property to None.
In this sample, we are creating custom objects and adding them to a list. We use the custom property to sort our list and after we add the sorted Items back into the ListView. The down side to this method is that you lose any group associations, therefore you need to reassign them. Plus this method isn’t the most efficient way of sorting your items. You are probably better off sorting the original data and rebuilding the ListView again.
Final Solution:
The best solution is to use a ListViewItemSorter class. Unfortunately it requires you to create a custom C# class that will handle the item comparisons. Fortunately for you, we wrote our own custom class to handle the sorting for you. The get around creating a class in PowerShell (something it doesn’t support), we have to use Add-Type cmdlet. The Add-Type cmdlet dynamically compiles C# code and allows you to use the new class directly in PowerShell.
Add-Type -ReferencedAssemblies ('System.Windows.Forms') -TypeDefinition @" using System; using System.Windows.Forms; using System.Collections; public class ListViewItemComparer : IComparer { public int column; public SortOrder sortOrder; public ListViewItemComparer() { column = 0; sortOrder = SortOrder.Ascending; } public ListViewItemComparer(int column, SortOrder sort) { this.column = column; sortOrder = sort; } public int Compare(object x, object y) { if(column >= ((ListViewItem)x).ListView.Columns.Count || column >= ((ListViewItem)x).SubItems.Count || column >= ((ListViewItem)y).SubItems.Count) column = 0; if(sortOrder == SortOrder.Ascending) return String.Compare(((ListViewItem)x).SubItems[column].Text,`
((ListViewItem)y).SubItems[column].Text);
else
return String.Compare(((ListViewItem)y).SubItems[column].Text,`
((ListViewItem)x).SubItems[column].Text); } } "@ | Out-Null
Our custom ListViewItemComparer class will act as a generic sorting class that will allow you to compare any column and choose in which direction to sort.
Next we created a PowerShell function that will allow you to sort the ListView by Column Index:
function Sort-ListViewColumn { <# .SYNOPSIS Sort the ListView's item using the specified column. .DESCRIPTION Sort the ListView's item using the specified column. This function uses Add-Type to define a class that sort the items. The ListView's Tag property is used to keep track of the sorting. .PARAMETER ListView The ListView control to sort. .PARAMETER ColumnIndex The index of the column to use for sorting. .PARAMETER SortOrder The direction to sort the items. If not specified or set to None, it will toggle. .EXAMPLE Sort-ListViewColumn -ListView $listview1 -ColumnIndex 0 #> param( [ValidateNotNull()] [Parameter(Mandatory=$true)] [System.Windows.Forms.ListView]$ListView, [Parameter(Mandatory=$true)] [int]$ColumnIndex, [System.Windows.Forms.SortOrder]$SortOrder = 'None') if(($ListView.Items.Count -eq 0) -or ($ColumnIndex -lt 0) -or ($ColumnIndex -ge $ListView.Columns.Count)) { return; } #region Define ListViewItemComparer try{ $local:type = [ListViewItemComparer] } catch{ Add-Type -ReferencedAssemblies ('System.Windows.Forms') -TypeDefinition @" using System; using System.Windows.Forms; using System.Collections; public class ListViewItemComparer : IComparer { public int column; public SortOrder sortOrder; public ListViewItemComparer() { column = 0; sortOrder = SortOrder.Ascending; } public ListViewItemComparer(int column, SortOrder sort) { this.column = column; sortOrder = sort; } public int Compare(object x, object y) { if(column >= ((ListViewItem)x).ListView.Columns.Count || column >= ((ListViewItem)x).SubItems.Count || column >= ((ListViewItem)y).SubItems.Count) column = 0; if(sortOrder == SortOrder.Ascending) return String.Compare(((ListViewItem)x).SubItems[column].Text,`
((ListViewItem)y).SubItems[column].Text);
else
return String.Compare(((ListViewItem)y).SubItems[column].Text,`
((ListViewItem)x).SubItems[column].Text); } } "@ | Out-Null } #endregion if($ListView.Tag -is [ListViewItemComparer]) { #Toggle the Sort Order if($SortOrder -eq [System.Windows.Forms.SortOrder]::None) { if($ListView.Tag.column -eq $ColumnIndex -and $ListView.Tag.sortOrder -eq 'Ascending') { $ListView.Tag.sortOrder = 'Descending' } else { $ListView.Tag.sortOrder = 'Ascending' } } else { $ListView.Tag.sortOrder = $SortOrder } $ListView.Tag.column = $ColumnIndex $ListView.Sort()#Sort the items } else { if($Sort -eq [System.Windows.Forms.SortOrder]::None) { $Sort = [System.Windows.Forms.SortOrder]::Ascending } #Set to Tag because for some reason in PowerShell ListViewItemSorter prop returns null $ListView.Tag = New-Object ListViewItemComparer ($ColumnIndex, $SortOrder) $ListView.ListViewItemSorter = $ListView.Tag #Automatically sorts } }
The first part checks if the ListViewItemComparer types exists. If it doesn’t then it will use the Add-Type cmdlets to add the custom class. Next the functions stores new sorter class in the ListView’s Tag property, so it can track which direction and column it was lasted sorted. This way it can easily toggle between Ascending and Descending when you click the same column.
The last step is to simply call the Sort-ListViewColumn function from ColumnClick event:
$listviewApplications_ColumnClick=[System.Windows.Forms.ColumnClickEventHandler]{ #Event Argument: $_ = [System.Windows.Forms.ColumnClickEventArgs] Sort-ListViewColumn $this $_.Column }
Now you need not worry about creating your own custom sorting script. Just call Sort-ListViewColumn and your done.
Sorted Columns:
ListView Help Function:
Here is a help function to add items to the ListView. It will handle the creation of ListViewItems for you.
function Add-ListViewItem { Param( [ValidateNotNull()] [Parameter(Mandatory=$true)] [System.Windows.Forms.ListView]$ListView, [ValidateNotNull()] [Parameter(Mandatory=$true)] $Items, [int]$ImageIndex = -1, [string[]]$SubItems, [System.Windows.Forms.ListViewGroup]$Group, [switch]$Clear) if($Clear) { $ListView.Items.Clear(); } if($Items -is [Array]) { foreach ($item in $Items) { $listitem = $ListView.Items.Add($item.ToString(), $ImageIndex) #Store the object in the Tag $listitem.Tag = $item if($SubItems -ne $null) { $listitem.SubItems.AddRange($SubItems) } if($Group -ne $null) { $listitem.Group = $Group } } } else { #Add a new item to the ListView $listitem = $ListView.Items.Add($Items.ToString(), $ImageIndex) #Store the object in the Tag $listitem.Tag = $Items if($SubItems -ne $null) { $listitem.SubItems.AddRange($SubItems) } if($Group -ne $null) { $listitem.Group = $Group } } }
Note: These helper functions are already included in PrimalForms 2011 since v2.0.20.
We also created a sample form demonstrates the ListView and column sorting. You can download the ListView sample from our Downloads section.
If you use the sort-listviewcolumn function and then clear all the items from the listview and try to repopulate the listview you will get an exception “invalidargument=value of x is not valid for ‘index’. Where x is the column you sorted on before clearing the listview. ?????
I updated the Sort-ListViewColumn function in the blog to address the issue. It will now check the item count before attempting to sort. Thank you for reporting the issue.
it doesn’t like – if(col >= ((ListViewItem)x).ListView.Items.Count)
return 0;
col generates an error, maybe intended column? However it seems like this is checking column # against item count?
Sorry remove those two lines. Forgot to update both sections of the blog.
if(column >= ((ListViewItem)x).ListView.Columns.Count)
column = 0;
Still getting an exception if the listview has been sorted on a column and then all the items are cleared and you try and add new items to the listview. Is there a way to unlink the sort before clearing and adding new items? I can always call the sort column function after all items have been added. Don’t really need it to do it for each item added anyway – especially since beginupdate has already been called and endupdate will be called only after all items have been added.
Thanks David
Set the ListView’s Sorting property to None. Or Set the ListView’s ListViewItemSorter property to $null. Please make sure you copy the latest version. Had some copy paste issues with the wrong script 🙂
I should have mentioned that the exception happens for each new item added to the listview. What you did fix with the change is that the exception now does not happen on the very first item added but still happens for every subsequent item added.
Ok I believe this has to do with the subitems not matching the number of columns.
I updated the Compare class as follows:
if(column >= ((ListViewItem)x).ListView.Columns.Count ||
column >= ((ListViewItem)x).SubItems.Count ||
column >= ((ListViewItem)y).SubItems.Count)
column = 0;
Thanks for the help David. This may have been related to the fact that I am not using the add-listviewitem function to add items? Anyway I tried setting listviewitemsorter to $null before clearing the items and re-adding the new items. That stopped the exceptions but would not sort again after adding the new items. So I tried saving the value the listviewitemsorter before setting it to $null and then replacing listviewitemsorter with the saved value after adding all the new items. $sucess
Thanks again