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
|
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
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.
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
SQL-Server table columns and related tables
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
Source code
The following GitHub repository contains all code for this article.