Share via


Windows Forms ListView: Examining Detail view with SQL-Server (VB.NET)

Introduction

Although Microsoft documentation for the ListView class is well written along with many code snippets on the Internet there are no well-rounded code samples to follow for populating a ListView control from start to finish reading from a data source.  

The following code samples provide guidance to populate a ListView from both SQL-Server database table and Json files. Focus is on working with ListView control in Details view, ListView Groups and checked rows.

Intended audience 

Hobbyist or developer with basic understanding of working with SQL-Server and a firm grasp working with VB.NET.

Basic setting: ListView control at design time

With a Window form selected and the Toolbox visible double click on a ListView, resize the ListView to accommodate data intended to populate the ListView. On the top right-hand corner of the ListView single click the chevron. From the view dropdown select Details. Click “Edit columns”. For each item .e.g. first name, last name click the “Add” button, name the column e.g. FirstNameColumn, set the Text property e.g. First Name, when done press OK. Don’t be concerned with column width at this time, this will be done later.

Basic detail view: Contacts Example

Create a class which represents data read from a database table. In the following example customer contacts of type owner will be the target for the first example. This class is a partial representation of several joined tables.

Namespace Classes
    Public Class  Contact
        Public Property  CustomerIdentifier() As Integer
        Public Property  CompanyName() As  String
        Public Property  FirstName() As  String
        Public Property  LastName() As  String
        Public Property  PhoneTypeDescription() As String
        Public Property  PhoneNumber() As  String
        Public Property  CountryName() As  String
 
        Public ReadOnly  Property ItemArray() As String()
            Get
                Return {CompanyName, FirstName, LastName, PhoneNumber, CountryName}
            End Get
        End Property
 
    End Class
End Namespace

The ItemArray property will be used to add items to the ListView as the Add method accepts an array of items. The follow method is in a class solely responsible for reading data from a SQL-Server database.

Important
  • The SELECT statement uses embedded expressions which allows a developer to write SQL statements in SQL-Server and use them in code without the need to create a string with the SQL.
  • The WHERE condition is the embedded expression which could had been a parameter.
Public Function  GetOwnerContacts() As List(Of Contact)
    mHasException = False
 
    Dim ownerContacts = New List(Of Contact)()
 
 
    Dim contactTypeIdentifier As Integer  = 7
 
    Dim selectStatement =
            <SQL>
            SELECT   Cust.CustomerIdentifier ,
                     Cust.CompanyName ,
                     cont.FirstName ,
                     cont.LastName ,
                     PT.PhoneTypeDescription ,
                     CCD.PhoneNumber ,
                     Countries.CountryName
            FROM     Customers AS Cust
                     INNER JOIN dbo.Contact AS cont ON Cust.ContactIdentifier = 
                        cont.ContactIdentifier
                     INNER JOIN dbo.ContactContactDevices AS CCD ON cont.ContactIdentifier = 
                        CCD.ContactIdentifier
                     INNER JOIN dbo.PhoneType AS PT ON CCD.PhoneTypeIdenitfier = 
                        PT.PhoneTypeIdenitfier
                     INNER JOIN dbo.Countries ON Cust.CountryIdentfier = Countries.id
            WHERE    ( Cust.ContactTypeIdentifier = <%= contactTypeIdentifier %> )
            ORDER BY Cust.CompanyName;
            </SQL>.Value
 
 
    Using cn = New  SqlConnection() With  {.ConnectionString = ConnectionString}
        Using cmd = New  SqlCommand() With  {.Connection = cn}
            Try
                cn.Open()
 
                cmd.CommandText = selectStatement
 
                Dim reader = cmd.ExecuteReader()
                Do While  reader.Read()
 
                    ownerContacts.Add(New Contact() With
                     {
                         .CustomerIdentifier = reader.GetInt32(0),
                         .CompanyName = reader.GetString(1),
                         .FirstName = reader.GetString(2),
                         .LastName = reader.GetString(3),
                         .PhoneTypeDescription = reader.GetString(4),
                         .PhoneNumber = reader.GetString(5),
                         .CountryName = reader.GetString(6)
                     })
                Loop
 
            Catch e As Exception
                mHasException = True
                mLastException = e
            End Try
        End Using
    End Using
 
    Return ownerContacts
 
End Function

In the form with the ListView, in form shown event a new instance of the data class is created followed by call the method above. To ensure that any screen updates are not shown (from adding and resizing columns) BeginUpdate is invoked before adding items to the ListView. If there is a chance of a runtime exception the code between BeginUpDate and the line above EndUpdate may be wrapped in a try-finally statement so that the ListView is partly populated is in working order although the ListView may be unstable so better to prevent any other operations to proceed as seen fit.

Private Sub  ContactForm_Shown(sender As Object, e As  EventArgs) _
    Handles Me.Shown
 
    Dim dataOperations = New SqlInformation()
    Dim contacts = dataOperations.GetOwnerContacts()
 
    ownerContactListView.BeginUpdate()
 
    For Each  contact In  contacts
 
        ownerContactListView.Items.Add(
            New ListViewItem(contact.ItemArray) With
                {
                    .Tag = contact.CustomerIdentifier
                })
 
    Next
 
    ownerContactListView.AutoResizeColumns(
        ColumnHeaderAutoResizeStyle.HeaderSize)
 
    ownerContactListView.EndUpdate()
 
    ownerContactListView.FocusedItem = ownerContactListView.Items(0)
    ownerContactListView.Items(0).Selected = True
    ActiveControl = ownerContactListView
 
End Sub

During the iteration of contacts a new row is added to the ListView using an overload which accepts an object array which is possible using the Contact ItemArray method. Rather then create an instance of a ListViewItem as a variable and then passing the variable to the Add method it's more efficient to simple add the item directly in the add method. The Tag property is set to the primary key of the contact which is available later to get more information from the database tables. In the following code sample the Tag property for the current item is cast to a integer which can be used to reference back to the table customers.

Dim id = Convert.ToInt32(ownerContactListView.SelectedItems(0).
                            Tag.ToString())

Sizing of columns is performed using the following line which sizes per data which has been added to the ListView.

ownerContactListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize)

To get details for the current ListView Item, in this case the ListView MouseDoubleClick event. The contact and phone will be displayed in a message dialog.

Private Sub  ownerContactListView_MouseDoubleClick(
    sender As  Object,
    e As  MouseEventArgs) _
    Handles ownerContactListView.MouseDoubleClick
 
    MessageBox.Show(
        $"Call {ownerContactListView.SelectedItems(0).Text} at " &
        $"{ownerContactListView.SelectedItems(0).SubItems(3).Text}")
 
End Sub

Screenshot

Basic Detail view with grouping: Products/Category example

In a conventional view for selecting products a DataGridView is used with a filtering mechanism to show one category at a time while an alternate approach would be to group products by category which a standard DataGridView is not capable of without a fair amount of custom coding or a third party grid control. A ListView is the perfect fit when multiple groups (or in this case categories) need to be displayed together.

Step 1: Obtain items which represent groups

This demonstration groups products by categories. The following class is used to create a list for working with groups.

Namespace Classes
    Public Class  Category
        Public Property  CategoryId() As  Integer
        Public Property  Name() As  String
        Public Overrides  Function ToString() As String
            Return Name
        End Function
    End Class
End Namespace

In the form shown event the data class is created and a call is made to obtain categories.

Dim dataOperations = New SqlInformation()
Dim categories = dataOperations.Categories()

Code to obtain categories from a database table.

Public Function  Categories() As  List(Of Category)
    mHasException = False
 
    Dim categoryList = New List(Of Category)()
    Dim selectStatement = "SELECT CategoryID,CategoryName FROM dbo.Categories"
 
    Using cn = New  SqlConnection() With  {.ConnectionString = ConnectionString}
        Using cmd = New  SqlCommand() With  {.Connection = cn}
 
            cmd.CommandText = selectStatement
 
            Try
 
                cn.Open()
 
                Dim reader = cmd.ExecuteReader()
 
                Do While  reader.Read()
                    categoryList.Add(New Category() With
                        {
                            .CategoryId = reader.GetInt32(0),
                            .Name = reader.GetString(1)
                        })
                Loop
 
            Catch e As Exception
                mHasException = True
                mLastException = e
            End Try
        End Using
    End Using
 
    Return categoryList
 
End Function

Step 2: Populate ListView

For each category products for the current category are read from the database tables using the following method in the data class.

Public Function  Products(Identifier As Integer) As  List(Of Product)
    mHasException = False
 
    Dim productList = New List(Of Product)()
 
    Dim selectStatement =
            <SQL>
            SELECT   P.ProductID ,
                     P.ProductName ,
                     P.SupplierID ,
                     S.CompanyName AS Supplier ,
                     S.Phone ,
                     P.CategoryID ,
                     P.UnitPrice ,
                     P.UnitsInStock
            FROM     dbo.Products AS P
                     INNER JOIN dbo.Suppliers AS S ON P.SupplierID = S.SupplierID
            WHERE    P.CategoryID = @CategoryIdentifier
            ORDER BY P.ProductName;
            </SQL>.Value
 
    Using cn = New  SqlConnection() With  {.ConnectionString = ConnectionString}
        Using cmd = New  SqlCommand() With  {.Connection = cn}
 
            cmd.CommandText = selectStatement
            cmd.Parameters.AddWithValue("@CategoryIdentifier", Identifier)
 
            Try
                cn.Open()
                Dim reader = cmd.ExecuteReader()
                Do While  reader.Read()
 
                    productList.Add(New Product() With
                       {
                           .ProductId = reader.GetInt32(0),
                           .ProductName = reader.GetString(1),
                           .SupplierId = reader.GetInt32(2),
                           .Supplier = reader.GetString(3),
                           .Phone = reader.GetString(4),
                           .CategoryId = reader.GetInt32(5),
                           .UnitPrice = reader.GetDecimal(6),
                           .UnitsInStock = reader.GetInt16(7)
                       })
 
                Loop
            Catch e As Exception
                mHasException = True
                mLastException = e
            End Try
        End Using
    End Using
 
    Return productList
End Function

Definition of the Product class.

Namespace Classes
    Public Class  Product
        Public Property  ProductId() As  Integer
        Public Property  ProductName() As  String
        Public Property  SupplierId() As  Integer
        Public Property  Supplier() As  String
        Public Property  Phone() As  String
        Public Property  CategoryId() As  Integer
        Public Property  UnitPrice() As  Decimal?
        Public Property  UnitsInStock() As  Integer
        ''' <summary>
        ''' Container for ListView item to store primary keys
        ''' </summary>
        Public ReadOnly  Property IdentifiersTag() As ProductTag
            Get
                Return New  ProductTag() With
                    {
                        .CategoryId = CategoryId,
                        .SupplierId = SupplierId,
                        .ProductId = ProductId
                    }
            End Get
        End Property
 
        Public Overrides  Function ToString() As String
            Return $"{ProductName}"
        End Function
    End Class
End Namespace

Note IdentifiersTag property contains primary and foreign keys for use later if needed to query for more information of fields not obtained in the read operation above or for when edits are needed.

Namespace Classes
    ''' <summary>
    ''' Container to store primary keys in a ListViewItem used
    ''' in the project BasicListViewGroupsExample
    ''' </summary>
    Public Class  ProductTag
        Public Property  ProductId() As  Integer
        Public Property  SupplierId() As  Integer
        Public Property  CategoryId() As  Integer
    End Class
End Namespace

Once products have been returned from the database a name is composed for the current group which becomes the name of the group (ListViewGroup) along with a indexer to keep group names unique.  Unlike the first example which used an array to populate a list item this example sets each sub-item directly in the constructor for adding a new item along with assigning the group and within the Tag property keys for referencing back to the tables in the database.

For some simple formatting if a product has nothing in stock the units in stock has a foreground color of bold red. Finally the list item is added to the ListView.

Private Sub  MainForm_Shown(sender As Object, e As  EventArgs) Handles  Me.Shown
 
    Dim dataOperations = New SqlInformation()
    Dim categories = dataOperations.Categories()
 
    Dim categoryIndex = 1
 
    ' ReSharper disable once TooWideLocalVariableScope
    Dim groupName = ""
 
    For Each  category In  categories
        Dim products = dataOperations.Products(category.CategoryId)
 
        '                
        ' Some category names have unwanted characters and/or whitespace, remove these chars.
        '                 
        groupName = category.Name.Replace("/", "").Replace(" ", "")
 
        Dim currentGroup = New ListViewGroup(category.Name, HorizontalAlignment.Left) With  {
                .Header = category.Name,
                .Name = $"{groupName}Group{categoryIndex}"
            }
 
        categoryIndex += 1
 
        ProductListView.Groups.Add(currentGroup)
 
        For Each  product In  products
            Dim listViewProductItem = New ListViewItem(
                {
                    product.ProductName,
                    product.Supplier,
                    product.Phone,
                    product.UnitPrice.ToString(),
                    product.UnitsInStock.ToString()
                }, -1)
 
            listViewProductItem.Group = currentGroup
            listViewProductItem.Tag = product.IdentifiersTag
            listViewProductItem.UseItemStyleForSubItems = False
 
 
            If product.UnitsInStock = 0 Then
 
                listViewProductItem.SubItems(4).ForeColor = Color.Red
 
                listViewProductItem.SubItems(4).Font =
                    New Font(
                        listViewProductItem.SubItems(4).Font.FontFamily,
                        listViewProductItem.SubItems(4).Font.Size, FontStyle.Bold)
 
            End If
 
            ProductListView.Items.Add(listViewProductItem)
 
        Next
    Next
 
    ProductListView.FocusedItem = ProductListView.Items(0)
    ProductListView.Items(0).Selected = True
 
    ActiveControl = ProductListView
 
    AddHandler ProductListView.ItemSelectionChanged, AddressOf ProductListView_ItemSelectionChanged
    AddHandler ProductListView.ItemCheck, AddressOf ProductListView_ItemCheck
 
    GroupsComboBox.DataSource = ProductListView.Groups.Cast(Of ListViewGroup)().Select(Function(lvg) lvg.Name).ToList()
 
End Sub

Once all items have been added the first item is focused.

Screenshot

Checked rows

To add check boxes, select the ListView, select properties, find CheckBoxes and set to true. 

Iterate checked rows

For this example the code is in a button click which writes results to the IDE output window while in a production all there would be a list to pass to business logic for processing.

Private Sub  GetGroupProductsButton_Click(sender As Object, e As  EventArgs) _
    Handles GetGroupProductsButton.Click
 
    Dim sb As New  StringBuilder
 
    Dim specificGroup = ProductListView.Groups.Cast(Of ListViewGroup)().
            FirstOrDefault(Function(lvg) lvg.Name = GroupsComboBox.Text)
 
    For index As Integer  = 0 To  specificGroup.Items.Count - 1
        Dim productTag = specificGroup.Items(index).ProductTag()
        sb.AppendLine($"Id: {productTag.ProductId} Product: {specificGroup.Items(index).Text}")
    Next
 
    Dim viewerForm As New  UtilityViewerForm(sb.ToString())
    viewerForm.Text = $"Category: {GroupsComboBox.Text}"
 
    Try
        viewerForm.ShowDialog()
    Finally
        viewerForm.Dispose()
    End Try
 
End Sub

Take note on var productTag = specificGroup.Items[index].ProductTag(); where ProductTag is a language extension method within the current project. This keeps the cast process out of sight for cleaner code in the form.

Imports SqlServerOperations.Classes
 
Namespace LanguageExtensions
    ''' <summary>
    ''' Contains project specific extension methods
    ''' </summary>
    Public Module  ListViewExtensions
        ''' <summary>
        ''' Get primary and foreign keys for a product
        ''' within a ListView
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <returns></returns>
        <System.Runtime.CompilerServices.Extension>
        Public Function  ProductTag(sender As  ListViewItem) As  ProductTag
            Return DirectCast(sender.Tag, ProductTag)
        End Function
    End Module
End Namespace

Another place the extension method is useful for is when traversing rows in the ListView, here other details may be obtained using the keys stored in the current item tag property. Here keys are simply displayed in labels.

Private Sub  ProductListView_ItemSelectionChanged(
     sender As  Object,
     e As  ListViewItemSelectionChangedEventArgs)
 
    If e.IsSelected Then
 
        Dim primaryKeys = e.Item.ProductTag()
 
        SelectionChangedLabel.Text =
            $"product: {primaryKeys.ProductId} " &
            $"{ProductListView.Items(e.ItemIndex).Text}"
 
    End If
End Sub

The following code shows how to collect checked row items into a child form.

Private Sub  SelectedProductsButton_Click(sender As Object, e As  EventArgs) _
    Handles SelectedProductsButton.Click
 
    Dim checkedItems = ProductListView.CheckedItems
 
    If checkedItems.Count > 0 Then
        Dim sb = New StringBuilder()
 
        For index As Integer  = 0 To  checkedItems.Count - 1
            Dim keys = checkedItems(index).ProductTag()
 
            sb.AppendLine(
                $"{keys.CategoryId}," &
                $"{checkedItems(index).Group.Header}," &
                $"{keys.ProductId}," &
                $"{checkedItems(index).Text}," &
                $"{keys.SupplierId}," &
                $"{checkedItems(index).SubItems(0).Text}")
 
        Next
 
        Dim f = New SelectedProductsForm(sb.ToString())
 
        Try
            f.ShowDialog()
        Finally
            f.Dispose()
        End Try
    Else
        MessageBox.Show("No product(s) selected")
    End If
 
End Sub

Other examples

In the following example which follows the same code paths as the examples shown above a ListView is setup for displaying table column definitions and related tables.

The following class is responsible for table information.

Namespace Classes
    Public Class  ServerTableItem
        Public Property  Table() As  String
        Public Property  Field() As  String
        Public Property  FieldOrder() As  Int16?
        Public Property  DataType() As  String
        Public Property  Length() As  Int16?
        Public Property  Precision() As  String
        Public Property  Scale() As  Int32
        Public Property  AllowNulls() As  String
        Public Property  Identity() As  String = String.Empty
        Public Property  PrimaryKey() As  String
        Public Property  ForeignKey() As  String = String.Empty
        Public Property  RelatedTable() As  String
        Public Property  Description() As  String
        Public Overrides  Function ToString() As String
            Return Field
        End Function
    End Class
End Namespace

Method which returns table information explained above.

Public Function  TableDependencies() As Dictionary(Of String, List(Of ServerTableItem))
    mHasException = False
 
    Dim selectStatement =
            <SQL>
            SELECT  syso.name [Table],
                    sysc.name [Field], 
                    sysc.colorder [FieldOrder], 
                    syst.name [DataType], 
                    sysc.[length] [Length], 
                    CASE WHEN sysc.prec IS NULL THEN 'NULL' ELSE CAST(sysc.prec AS VARCHAR) END [Precision],
            CASE WHEN sysc.scale IS null THEN '-' ELSE sysc.scale END [Scale], 
            CASE WHEN sysc.isnullable = 1 THEN 'True' ELSE 'False' END [AllowNulls], 
            CASE WHEN sysc.[status] = 128 THEN 'True' ELSE 'False' END [Identity], 
            CASE WHEN sysc.colstat = 1 THEN 'True' ELSE 'False' END [PrimaryKey],
            CASE WHEN fkc.parent_object_id is NULL THEN 'False' ELSE 'True' END [ForeignKey], 
            CASE WHEN fkc.parent_object_id is null THEN '(none)' ELSE obj.name  END [RelatedTable],
            CASE WHEN ep.value is NULL THEN '(none)' ELSE CAST(ep.value as NVARCHAR(500)) END [Description]
            FROM [sys].[sysobjects] AS syso
            JOIN [sys].[syscolumns] AS sysc on syso.id = sysc.id
            LEFT JOIN [sys].[systypes] AS syst ON sysc.xtype = syst.xtype and syst.name != 'sysname'
            LEFT JOIN [sys].[foreign_key_columns] AS fkc on syso.id = fkc.parent_object_id and 
                sysc.colid = fkc.parent_column_id    
            LEFT JOIN [sys].[objects] AS obj ON fkc.referenced_object_id = obj.[object_id]
            LEFT JOIN [sys].[extended_properties] AS ep ON syso.id = ep.major_id and sysc.colid = 
                ep.minor_id and ep.name = 'MS_Description'
            WHERE syso.type = 'U' AND  syso.name != 'sysdiagrams'
            ORDER BY [Table], FieldOrder, Field;
         </SQL>.Value
 
    Dim informationTable = New DataTable()
 
    Using cn = New  SqlConnection() With  {.ConnectionString = ConnectionString}
        Using cmd = New  SqlCommand() With  {.Connection = cn}
 
            cmd.CommandText = selectStatement
 
            Try
 
                cn.Open()
 
                informationTable.Load(cmd.ExecuteReader())
            Catch e As Exception
                mHasException = True
                mLastException = e
            End Try
 
        End Using
    End Using
 
    Dim anonymousResult = From row In informationTable.AsEnumerable()
                          Group row By groupKey = row.Field(Of String)("Table") Into grp = Group
                          Select New  With
                {
                    Key .TableName = groupKey,
                    Key .Rows = grp,
                    Key .Count = grp.Count()
                }
 
    Dim tableDictionary = New Dictionary(Of String, List(Of ServerTableItem))()
 
    For Each  topItem In  anonymousResult
 
        If Not  tableDictionary.ContainsKey(topItem.TableName) Then
            tableDictionary(topItem.TableName) = New  List(Of ServerTableItem)()
        End If
 
        For Each  row In  topItem.Rows
            tableDictionary(topItem.TableName).Add(
                New ServerTableItem() With
                  {
                      .Table = topItem.TableName,
                      .Field = row.Field(Of String)("Field"),
                      .FieldOrder = row.Field(Of Short)("FieldOrder"),
                      .DataType = row.Field(Of String)("DataType"),
                      .Length = row.Field(Of Short)("Length"),
                      .Precision = row.Field(Of String)("Precision"),
                      .Scale = row.Field(Of Integer)("Scale"),
                      .AllowNulls = row.Field(Of String)("AllowNulls"),
                      .Identity = row.Field(Of String)("Identity"),
                      .PrimaryKey = row.Field(Of String)("PrimaryKey"),
                      .ForeignKey = row.Field(Of String)("ForeignKey"),
                      .RelatedTable = row.Field(Of String)("RelatedTable"),
                      .Description = row.Field(Of String)("Description")
                  })
        Next
    Next
 
    Return tableDictionary
 
End Function

The Dictionary returned above is set to a ComboBox data source where a BindingSource is required as a ComboBox.DataSource does not handle dictionaries.

Private Sub  Form1_Shown(sender As Object, e As  EventArgs) Handles  Me.Shown
 
    AddHandler listView1.ItemSelectionChanged, AddressOf ListView1_ItemSelectionChanged
 
    Dim items = _tableInformation.TableDependencies()
 
    tableInformationComboBox.DataSource = New  BindingSource(items, Nothing)
    tableInformationComboBox.DisplayMember = "Key"
 
End Sub

When a selection is made from the ComboBox, clicking the associated button populates the ListView using a more drawn out method by adding SubItems rather than using an array to setup a ListViewItem.

Once the load process has completed the first item is selected followed by painting alternate rows in a light grey.

Private Sub  GetInformationButton_Click(sender As Object, e As  EventArgs) Handles  GetInformationButton.Click
 
    listView1.Items.Clear()
 
    Dim detailItems = (CType(tableInformationComboBox.SelectedItem, KeyValuePair(Of String, List(Of ServerTableItem))))
 
    For Each  serverTableItem In  detailItems.Value
        Dim item = listView1.Items.Add(serverTableItem.FieldOrder.ToString())
        item.SubItems.Add(serverTableItem.Field)
        item.SubItems.Add(serverTableItem.DataType)
        item.SubItems.Add(serverTableItem.Length.ToString())
        item.SubItems.Add(serverTableItem.Precision)
        item.SubItems.Add(serverTableItem.Scale.ToString())
        item.SubItems.Add(serverTableItem.AllowNulls)
        item.SubItems.Add(serverTableItem.Identity)
        item.SubItems.Add(serverTableItem.PrimaryKey)
        item.SubItems.Add(serverTableItem.ForeignKey)
        item.SubItems.Add(serverTableItem.RelatedTable)
 
        item.Tag = serverTableItem.Description
    Next
 
    listView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize)
    listView1.FocusedItem = listView1.Items(0)
    listView1.Items(0).Selected = True
    ActiveControl = listView1
 
    '            
    ' * Shade alternate rows
    '             
    Dim index = 0
    Dim shadedBackgroundColor = Color.FromArgb(240, 240, 240)
 
    For Each  item As  ListViewItem In  listView1.Items
 
        If index Mod 2 <> 1 Then
            index += 1
            Continue For
        Else
            index += 1
        End If
 
        item.BackColor = shadedBackgroundColor
        item.UseItemStyleForSubItems = True
    Next
 
End Sub

Working with moving rows up/down


This example provide functionality to move rows up/down using a language extension called in the two buttons to the left of the ListView. The ListView data in this case comes from a Json file although the data source does not matter as data can also be read from a database.

Class to read a Json file utilizing Newtonsoft.json.

Imports System.IO
Imports Newtonsoft.Json
 
''' <summary>
''' Responsible, using JsonNet to read and write json
''' from a json file to and from a concrete class
''' </summary>
Public Class  FileOperations
    ''' <summary>
    ''' Read json file into a list which will be passed to
    ''' a form to load into a ListView
    ''' </summary>
    ''' <param name="FileName">File to read json from</param>
    ''' <returns></returns>
    Public Function  LoadApplicationData(FileName As String) As  List(Of Application)
 
        Using streamReader = New  StreamReader(FileName)
            Dim json = streamReader.ReadToEnd()
            Return JsonConvert.DeserializeObject(Of List(Of Application))(json)
        End Using
 
    End Function
    ''' <summary>
    ''' Takes a list of application stored in a ListView and save the
    ''' data to a json file.
    ''' </summary>
    ''' <param name="Applications">List of Application</param>
    ''' <param name="FileName">Path and file name to save the list oo</param>
    Public Sub  SaveApplicationData(Applications As List(Of Application), FileName As String)
 
        Using streamWriter = File.CreateText(FileName)
 
            Dim serializer = New JsonSerializer With {.Formatting = Formatting.Indented}
            serializer.Serialize(streamWriter, Applications)
 
        End Using
 
    End Sub
 
End Class

Which uses the following class to hold the Json data. Note a similar method, ItemArray which is used in the loading of data into the ListView.

''' <summary>
''' Concrete container for json data in project JsonExample
''' </summary>
Public Class  Application
    Public Property  id() As  Integer
    Public Property  ApplicationName() As  String
    Public Property  ApplicationVersion() As String
    Public Property  ApplicationKey() As  String
    ''' <summary>
    ''' Used to load ListView items in ListViewJsonExampleForm
    ''' in project JsonExample.
    ''' </summary>
    ''' <returns></returns>
    Public Function  ItemArray() As  String()
        Return {ApplicationName, ApplicationVersion, ApplicationKey}
    End Function
 
End Class

In Form Shown event data returned from the Json file now in a list is iterated through to populate the ListView.

Private Sub  ListViewJsonExampleForm_Shown(sender As Object, e As  EventArgs) Handles  Me.Shown
 
    listView1.View = View.Details
 
    listView1.GridLines = True
    listView1.OwnerDraw = False
    listView1.FullRowSelect = True
 
    'Add column header
    listView1.Columns.Add("NameColumn", 200)
    listView1.Columns.Add("VersionColumn", 130)
    listView1.Columns.Add("KeyColumn", 160)
 
    listView1.Columns(0).Text = "Name"
    listView1.Columns(1).Text = "Version"
    listView1.Columns(2).Text = "Key"
 
    Dim applicationList = _fileOperations.LoadApplicationData(_fileName)
 
    '            
    ' * Add each item from json and set the identifier using the tag property
    ' * where the identifier is used to save item positions in SavePositionsButton.
    '             
    For Each  app In  applicationList
        Dim item = New ListViewItem(app.ItemArray()) With {.Tag = app.id}
        listView1.Items.Add(item)
    Next
 
    listView1.Items(0).Selected = True
    listView1.Select()
 
End Sub

To move ListView rows up/down the following extension method is used which is within a class project so it can be used in other projects.

Public Module  ListViewExtensions
    ''' <summary>
    ''' Move row up or down dependent on direction parameter
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="moveDirection">Up or Down</param>
    <Runtime.CompilerServices.Extension>
    Public Sub  MoveListViewItems(sender As ListView, moveDirection As MoveDirection)
 
        Dim direction As Integer  = moveDirection
 
        Dim valid As Boolean  = sender.SelectedItems.Count > 0 AndAlso
               ((moveDirection = MoveDirection.Down AndAlso
                 (sender.SelectedItems(sender.SelectedItems.Count - 1).Index < sender.Items.Count - 1)) OrElse
                (moveDirection = MoveDirection.Up AndAlso  (sender.SelectedItems(0).Index > 0)))
 
        If valid Then
 
            sender.SuspendLayout()
 
            Try
                For Each  item As  ListViewItem In  sender.SelectedItems
 
                    Dim index = item.Index + direction
                    sender.Items.RemoveAt(item.Index)
                    sender.Items.Insert(index, item)
                    sender.Items(index).Selected = True
                    sender.Focus()
 
                Next
            Finally
 
                sender.ResumeLayout()
 
            End Try
        End If
    End Sub
End Module

Implementation of extension method for moving rows up/down.

''' <summary>
''' Move current row unless first item
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub  upButton1_Click(sender As Object, e As  EventArgs) Handles  upButton1.Click
    listView1.MoveListViewItems(MoveDirection.Up)
End Sub
''' <summary>
''' Move row down unless last item.
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub  downButton1_Click(sender As Object, e As  EventArgs) Handles  downButton1.Click
    listView1.MoveListViewItems(MoveDirection.Down)
End Sub

Deleting selected rows

The following language extension method provides the base for removing selected rows.

Namespace LanguageExtensions
    Public Module  Extensions
        ''' <summary>
        ''' Provides the ability to remove selected rows in detail view.
        ''' </summary>
        ''' <param name="sender"></param>
        ''' <returns></returns>
        <Runtime.CompilerServices.Extension>
        Public Function  SelectedRows(
            sender As  ListView.ListViewItemCollection) As List(Of ListViewItem)
 
            Return sender.Cast(Of ListViewItem)().
                Where(Function(listViewItem) listViewItem.Selected).
                Select(Function(listViewItem) listViewItem).
                ToList()
 
        End Function
    End Module
End Namespace

Add the following class to the project which will be used to prompt the user if they are sure the selected rows should be removed.

Namespace My
    <ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)>
    Partial Friend  Class _Dialogs
        Public Function  Question(Text As  String) As Boolean
            Return (MessageBox.Show(
                Text,
                My.Application.Info.Title,
                MessageBoxButtons.YesNo,
                MessageBoxIcon.Question,
                MessageBoxDefaultButton.Button2) = MsgBoxResult.Yes)
        End Function
        Public Function  Question(Text As  String, Title As String) As  Boolean
            Return (MessageBox.Show(
                Text, Title,
                MessageBoxButtons.YesNo,
                MessageBoxIcon.Question,
                MessageBoxDefaultButton.Button2) = MsgBoxResult.Yes)
        End Function
    End Class
    <HideModuleName()>
    Friend Module  WinFormsDialogs
        Private instance As New  ThreadSafeObjectProvider(Of _Dialogs)
        ReadOnly Property  Dialogs() As  _Dialogs
            Get
                Return instance.GetInstance()
            End Get
        End Property
    End Module
End Namespace

The following code is found in this form in the supplied source code repository. Add a button for removing rows.

Private Sub  RemoveSelectedButton_Click(sender As Object, e As  EventArgs) _
    Handles RemoveSelectedButton.Click
 
    DeleteSelectedListViewRows()
 
End Sub

Then a method which can be called from other events or methods.

Private Sub  DeleteSelectedListViewRows()
 
    Dim selectedRows = ownerContactListView.Items.SelectedRows()
 
    If My.Dialogs.Question($"Remove {selectedRows.Count} rows?") Then
 
        For Each  listViewItem As  ListViewItem In  selectedRows
            ownerContactListView.Items.Remove(listViewItem)
        Next
 
    End If
End Sub

Finally the following method captures pressing the delete key.

Protected Overrides  Function ProcessCmdKey(ByRef msg As Message, keyData As Keys) As Boolean
    If keyData <> Keys.Delete Then
        Return MyBase.ProcessCmdKey(msg, keyData)
    End If
 
    DeleteSelectedListViewRows()
    Return True
 
End Function

Setting up code samples

  • From Solution Explorer execute "Restore NuGet Packages" (for Newtonsoft.Json and BaseConnectionLibrary)
  • Run the script DataScript.sql 
  • Change DatabaseServer to the name of your server e.g. .\SQLEXPRESS
  • Ensure Framework 4.7.2 or later is installed

Summary

This article has presented the basics for adding items to a ListView from database tables and Json for detail view using groups and check boxes for learning how to do this in Windows Forms projects.

Resources

Microsoft documentation for the ListView control
See also

C# version of this article

Source code

The following GitHub repository contains all code for this article.