Introduction
If you want to save the content of a ListView control there are many possibilities. You can serialize all items, you can loop through all items and
save them to an .ini-File, or you decide to write an Xml (Extensible Markup Language)-File
.
To my mind, the last option is the best. You have the control about all operations and so the function is very flexible and modifiable
.
Background
Once I needed to save the headers, the groups, and all their items contained in my ListView. Till then, I always used a serialization
function because
of the simple usage. But this only saved the items, and not the groups including their items. So I wrote my own method for saving and loading the whole content of a ListView control.
The logic behind the Code
Well, we decided to write an Xml-configuration-file
.
That’s quite easy, we’re going to loop through all headers, groups and their items.
That means:
There are several Xml-Nodes
:
- the document starts with
<ListView>
- then a Node named
<Columns>
contains one “Single-Line-Node” for each ColumnHeader; they save their most important data (the text, the width and the text alignment - the tag
<Groups>
contains a node for each group - within the
<Group>
tag containing the name and the header alignment, there is for each item another node with its subitems
Fair enough,
but what
does that mean
exactly?
When you save a ListView like this…
…the output configuration file looks as follows:
ListViews with groups aren’t the same as ListViews without groups!
Now, knowing how my function works, you must make differences between ListViews which use groups and those that don’t.
Why?
Perhaps you have noticed that the function first loops through each group, and then through its items. However, what’s going to happen if a
ListView has no groups? Because there is no group to search through its items, there cannot be found any. So the configuration file would be empty
.
For this reason, we have to write four differently working functions
(two for saving; two for loading). They differ in that the functions for ListViews
without groups come straight to the items and skip the groups, because they don’t exist.
Finally, we are talking about the code.
The ListViewContent-Class
Now you know the logic behind my function and that there are significant differences between a ListView that uses groups and one that doesn’t. So we
can consider how the file is written.
There are two Classes we could use from the System.Xml-Namespace
:
-
XmlDocument
XmlTextWriter
I chose the XmlTextWriter
because I think it’s a little bit easier to handle than the XmlDocument.
Let’s start with the function for saving the content of ListViews with groups.
ListViewWithGroupsSave
The code is commented, but I want to add something. It’s not difficult to understand. This function uses an XmlTextWriter
from the
System.Xml
-Namespace. Then it simply loops through all objects (headers, groups, items, etc.) we want to be saved.
Arguments:
-
lsv
: That’s the ListView control that’s going to be saved destinationPath
: The path you want to save the config file to.
Important:
You have to specify a file extension, such as .xml, .txt, .cfg or whatever you want.
Return Value:
If no errors occur the function returns an empty String, otherwise the Exception's Message.
Private Shared Function ListViewWithGroupsSave(ByVal lsv As ListView, ByVal destinationPath As String) As String
Try
Dim enc As New UnicodeEncoding
Dim XMLobj As XmlTextWriter = New XmlTextWriter(destinationPath, enc)
With XMLobj
.Formatting = Formatting.Indented
.Indentation = 4
.WriteStartDocument()
.WriteStartElement("ListView")
.WriteStartElement("Columns")
For i As Integer = 0 To lsv.Columns.Count - 1
.WriteStartElement("Column")
.WriteAttributeString("text", lsv.Columns(i).Text)
.WriteAttributeString("width", lsv.Columns(i).Width.ToString)
.WriteAttributeString("textAlign", lsv.Columns(i).TextAlign.ToString)
.WriteEndElement()
Next
.WriteEndElement()
.WriteStartElement("Groups")
For j As Integer = 0 To lsv.Groups.Count - 1
.WriteStartElement("Group")
.WriteAttributeString("name", lsv.Groups(j).Header)
.WriteAttributeString("headerAlignment", lsv.Groups(j).HeaderAlignment.ToString)
For k As Integer = 0 To lsv.Groups(j).Items.Count - 1
.WriteStartElement("item") .WriteAttributeString("text", lsv.Groups(j).Items(k).Text)
For l As Integer = 0 To lsv.Groups(j).Items(k).SubItems.Count - 2
.WriteStartElement("subItem") .WriteAttributeString("text", lsv.Groups(j).Items(k).SubItems(l + 1).Text)
.WriteEndElement()
Next
.WriteEndElement() Next
.WriteEndElement() Next
.WriteEndElement()
.WriteEndElement()
.WriteEndDocument()
.Close()
End With
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
ListViewWithGroupsLoad
In the function for loading, we use an XmlTextReader
. It isn’t much more complicated than the saving function, but I’m going to explain it in short.
There is a loop that goes on until the Reader has come to the end of the saved document. In each pass is queried what’s the current element. Thus,
step-by-step, all values are read in and added to the ListView.
Arguments:
lsv
: That’s the ListView control that’s going to be loadedloadedPath
: The path to the configuration file on the user’s filesystemappend
: "True" if you want to append the saved content to the ListView. "False" if you want the ListView to be cleared before loadingloadColumns
: Indicates if you want the HeaderColumn to be loaded.addInvisibleColumn
: Do you want that an "invisible" column will be added to the ListView? Perhaps you have already noticed that it isn’t
possible to align the first ColumnHeader’s text. So this option is very helpful. With an “invisible” first column with a width of 0 px, you can do
that. But don’t forget to “lock” its width by setting it to 0 px in the ListView.ColumnWidthChanging-Event
Return Value:
If no errors occur the function returns an empty String, otherwise the Exception's Message.
Private Shared Function ListViewWithGroupsLoad(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Try
If Not append Then lsv.Clear()
If loadColumns Then lsv.Columns.Clear()
If addInVisibleColumn Then lsv.Columns.Insert(0, "", 0)
Dim XMLReader As XmlReader = New XmlTextReader(loadedPath)
Dim grp As New ListViewGroup
Dim listItem As ListViewItem = New ListViewItem
With XMLReader
Do While .Read
If loadColumns Then
If .Name = "Column" Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("textAlign").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
lsv.Columns.Add(.GetAttribute("text"), Convert.ToInt32(.GetAttribute("width")), align)
End If
End If
If .Name = "Group" Then
If .IsStartElement Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("headerAlignment").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
grp = New ListViewGroup(.GetAttribute("name"), align)
lsv.Groups.Add(grp)
End If
End If
If .Name = "item" Then
If .IsStartElement Then
If addInVisibleColumn Then
listItem.SubItems.Add(.GetAttribute("text"))
Else
listItem.Text = .GetAttribute("text")
End If
Else
listItem.Group = grp
lsv.Items.Add(listItem)
listItem = New ListViewItem
End If
End If
If .Name = "subItem" Then
listItem.SubItems.Add(.GetAttribute("text"))
End If
Loop
.Close()
End With
For i As Integer = 0 To lsv.Groups.Count - 1
lsv.Groups(i).Name = lsv.Groups(i).Header
Next
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
ListViewWithNoGroupsSave
The function hasn’t to be explained, too, because it’s quite the same as ListViewWithNoGroupsSave
. It’s different that here’s just no loop for the
groups
; we directly go to the items.
The arguments and the return value are equal
to those of the other function.
Private Shared Function ListViewWithNoGroupsSave(ByVal lsv As ListView, ByVal destinationPath As String) As String
Try
Dim enc As New UnicodeEncoding
Dim XMLobj As XmlTextWriter = New XmlTextWriter(destinationPath, enc)
With XMLobj
.Formatting = Formatting.Indented
.Indentation = 4
.WriteStartDocument()
.WriteStartElement("ListView")
.WriteStartElement("Columns")
For i As Integer = 0 To lsv.Columns.Count - 1
.WriteStartElement("Column")
.WriteAttributeString("text", lsv.Columns(i).Text)
.WriteAttributeString("width", lsv.Columns(i).Width.ToString)
.WriteAttributeString("textAlign", lsv.Columns(i).TextAlign.ToString)
.WriteEndElement()
Next
.WriteEndElement()
.WriteStartElement("Items")
For k As Integer = 0 To lsv.Items.Count - 1
.WriteStartElement("item") .WriteAttributeString("text", lsv.Items(k).Text)
For l As Integer = 0 To lsv.Items(k).SubItems.Count - 2
.WriteStartElement("subItem") .WriteAttributeString("text", lsv.Items(k).SubItems(l + 1).Text)
.WriteEndElement()
Next
.WriteEndElement() Next
.WriteEndElement()
.WriteEndElement()
.WriteEndDocument()
.Close()
End With
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
ListViewWithNoGroupsLoad
The function also skips
the <Group>-tags, because they don’t exist.
The arguments and the return value are equal
to those of the other function.
Private Shared Function ListViewWithNoGroupsLoad(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Try
If Not append Then lsv.Clear()
If loadColumns Then lsv.Columns.Clear()
If addInVisibleColumn Then lsv.Columns.Insert(0, "", 0)
Dim XMLReader As XmlReader = New XmlTextReader(loadedPath)
Dim listItem As ListViewItem = New ListViewItem
With XMLReader
Do While .Read
If loadColumns Then
If .Name = "Column" Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("textAlign").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
lsv.Columns.Add(.GetAttribute("text"), Convert.ToInt32(.GetAttribute("width")), align)
End If
End If
If .Name = "item" Then
If .IsStartElement Then
If addInVisibleColumn Then
listItem.SubItems.Add(.GetAttribute("text"))
Else
listItem.Text = .GetAttribute("text")
End If
Else
lsv.Items.Add(listItem)
listItem = New ListViewItem
End If
End If
If .Name = "subItem" Then
listItem.SubItems.Add(.GetAttribute("text"))
End If
Loop
.Close()
End With
For i As Integer = 0 To lsv.Groups.Count - 1
lsv.Groups(i).Name = lsv.Groups(i).Header
Next
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
We have to identify whether the ListView to be saved has groups or not!
Since we now know how to handle those ListViews having groups and those that haven’t, we must detect which kind of ListView
the user wants to save.
Well, there are currently four functions in our “ListViewContent
”-class. Make sure that they are declared as “Private Shared
” and the class itself as
“Public MustInherit
”.
Why? I will explain later, at “Using the Code”.
Now we need two public shared functions
that the user will call. In these two functions we do not save or load anything, we are just going to detect if
the ListView control has groups or not. When this is done, we invoke the proper function.
Save
So, how can we detect the kind of the ListView to save?
We have access to all information we need from the ListView control. Therefore, the count of groups in a ListView that contains some groups, is greater
than zero. But we also have to check if the ListView does show any groups at all.
Putting this in Visual Basic .Net is not difficult:
Public Shared Function Save(ByVal lsv As ListView, ByVal destinationPath As String) As String
If lsv.Groups.Count > 0 AndAlso lsv.ShowGroups Then
Return ListViewWithGroupsSave(lsv, destinationPath)
Else
Return ListViewWithNoGroupsSave(lsv, destinationPath)
End If
End Function
If the given ListView has more than zero groups and it is also showing them, the function calls ListViewWithGroupsSave
, else it calls
ListViewWithNoGroupsSave
.
Then the return value is returned. If no error occurs it will be an empty String, otherwise the Exception’s message.
Load
It’s just a little bit more difficult to identify if the file to be loaded contains groups. There are two namespaces we need:
System.IO
-
System.Text.RegularExpressions
First, we need the content of the saved configuration file. We use the function File.ReadAllText
from the System.IO
-namespace and remove all line
breaks.
To find out if the file contains <group>-tags, we need the function RegEx.IsMatch
from the System.Text.RegularExpressions
-namespace. Regex is a
powerful tool for validating and editing Strings. IsMatch
checks a String for validity regarding a pattern.
That’s the very simple pattern we’ll use: (<groups>.*</groups>)|(<groups />)
-
The brackets are for grouping the two different expressions
-
This pattern searches for the tag <groups>…
-
A “.” stands for any character and the quantifier “*” indicates that the foregoing expression (“.” = any character) can
occur any number of times.
-
Then the group-tag is closed with </groups>
-
The pipe (|) finds an expression before OR after the symbol |.
-
This pattern also finds empty groups (<groups />)
Because of the pipe, this function returns “True” if either <groups>.*</groups> or <groups />
is found.
So we come to the VB-Code:
Public Shared Function Load(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Dim hasFileGroups As Boolean =
Regex.IsMatch(File.ReadAllText(loadedPath).Replace(Chr(10), ""), "(<groups>.*</groups>)|(<groups />)",
RegexOptions.IgnoreCase)
If hasFileGroups Then
Return ListViewWithGroupsLoad(lsv, loadedPath, append, loadColumns, addInVisibleColumn)
Else
Return ListViewWithNoGroupsLoad(lsv, loadedPath, append, loadColumns, addInVisibleColumn)
End If
End Function
If the saved configuration file contains the <group>
-tag, the function calls ListViewWithGroupsLoad
, else it calls ListViewWithNoGroupsLoad
Then the return value is returned. If no error occurs it will be an empty String, otherwise the Exception’s message.
The whole Class
Here is the full code of the ”ListViewContent“-Class:
Option Strict On
Imports System.Text
Imports System.Xml
Imports System.IO
Imports System.Text.RegularExpressions
Public MustInherit Class ListViewContent
#Region "Private Methods for saving and loading the content of a ListView"
#Region "Save the whole content of a ListView with groups into a file"
Private Shared Function ListViewWithGroupsSave(ByVal lsv As ListView, ByVal destinationPath As String) As String
Try
Dim enc As New UnicodeEncoding
Dim XMLobj As XmlTextWriter = New XmlTextWriter(destinationPath, enc)
With XMLobj
.Formatting = Formatting.Indented
.Indentation = 4
.WriteStartDocument()
.WriteStartElement("ListView")
.WriteStartElement("Columns")
For i As Integer = 0 To lsv.Columns.Count - 1
.WriteStartElement("Column")
.WriteAttributeString("text", lsv.Columns(i).Text)
.WriteAttributeString("width", lsv.Columns(i).Width.ToString)
.WriteAttributeString("textAlign", lsv.Columns(i).TextAlign.ToString)
.WriteEndElement()
Next
.WriteEndElement()
.WriteStartElement("Groups")
For j As Integer = 0 To lsv.Groups.Count - 1
.WriteStartElement("Group")
.WriteAttributeString("name", lsv.Groups(j).Header)
.WriteAttributeString("headerAlignment", lsv.Groups(j).HeaderAlignment.ToString)
For k As Integer = 0 To lsv.Groups(j).Items.Count - 1
.WriteStartElement("item") .WriteAttributeString("text", lsv.Groups(j).Items(k).Text)
For l As Integer = 0 To lsv.Groups(j).Items(k).SubItems.Count - 2
.WriteStartElement("subItem") .WriteAttributeString("text", lsv.Groups(j).Items(k).SubItems(l + 1).Text)
.WriteEndElement()
Next
.WriteEndElement() Next
.WriteEndElement() Next
.WriteEndElement()
.WriteEndElement()
.WriteEndDocument()
.Close()
End With
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
#End Region
#Region "Load the whole content of a ListView with groups from a file"
Private Shared Function ListViewWithGroupsLoad(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Try
If Not append Then lsv.Clear()
If loadColumns Then lsv.Columns.Clear()
If addInVisibleColumn Then lsv.Columns.Insert(0, "", 0)
Dim XMLReader As XmlReader = New XmlTextReader(loadedPath)
Dim grp As New ListViewGroup
Dim listItem As ListViewItem = New ListViewItem
With XMLReader
Do While .Read
If loadColumns Then
If .Name = "Column" Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("textAlign").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
lsv.Columns.Add(.GetAttribute("text"), Convert.ToInt32(.GetAttribute("width")), align)
End If
End If
If .Name = "Group" Then
If .IsStartElement Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("headerAlignment").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
grp = New ListViewGroup(.GetAttribute("name"), align)
lsv.Groups.Add(grp)
End If
End If
If .Name = "item" Then
If .IsStartElement Then
If addInVisibleColumn Then
listItem.SubItems.Add(.GetAttribute("text"))
Else
listItem.Text = .GetAttribute("text")
End If
Else
listItem.Group = grp
lsv.Items.Add(listItem)
listItem = New ListViewItem
End If
End If
If .Name = "subItem" Then
listItem.SubItems.Add(.GetAttribute("text"))
End If
Loop
.Close()
End With
For i As Integer = 0 To lsv.Groups.Count - 1
lsv.Groups(i).Name = lsv.Groups(i).Header
Next
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
#End Region
#Region "Save the whole content of a ListView without groups into a file"
Private Shared Function ListViewWithNoGroupsSave(ByVal lsv As ListView, ByVal destinationPath As String) As String
Try
Dim enc As New UnicodeEncoding
Dim XMLobj As XmlTextWriter = New XmlTextWriter(destinationPath, enc)
With XMLobj
.Formatting = Formatting.Indented
.Indentation = 4
.WriteStartDocument()
.WriteStartElement("ListView")
.WriteStartElement("Columns")
For i As Integer = 0 To lsv.Columns.Count - 1
.WriteStartElement("Column")
.WriteAttributeString("text", lsv.Columns(i).Text)
.WriteAttributeString("width", lsv.Columns(i).Width.ToString)
.WriteAttributeString("textAlign", lsv.Columns(i).TextAlign.ToString)
.WriteEndElement()
Next
.WriteEndElement()
.WriteStartElement("Items")
For k As Integer = 0 To lsv.Items.Count - 1
.WriteStartElement("item") .WriteAttributeString("text", lsv.Items(k).Text)
For l As Integer = 0 To lsv.Items(k).SubItems.Count - 2
.WriteStartElement("subItem") .WriteAttributeString("text", lsv.Items(k).SubItems(l + 1).Text)
.WriteEndElement()
Next
.WriteEndElement() Next
.WriteEndElement()
.WriteEndElement()
.WriteEndDocument()
.Close()
End With
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
#End Region
#Region "Load the whole content of a ListView without groups from a file"
Private Shared Function ListViewWithNoGroupsLoad(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Try
If Not append Then lsv.Clear()
If loadColumns Then lsv.Columns.Clear()
If addInVisibleColumn Then lsv.Columns.Insert(0, "", 0)
Dim XMLReader As XmlReader = New XmlTextReader(loadedPath)
Dim listItem As ListViewItem = New ListViewItem
With XMLReader
Do While .Read
If loadColumns Then
If .Name = "Column" Then
Dim align As HorizontalAlignment = 0
Select Case .GetAttribute("textAlign").ToLower
Case "left"
align = HorizontalAlignment.Left
Case "right"
align = HorizontalAlignment.Right
Case "center"
align = HorizontalAlignment.Center
End Select
lsv.Columns.Add(.GetAttribute("text"), Convert.ToInt32(.GetAttribute("width")), align)
End If
End If
If .Name = "item" Then
If .IsStartElement Then
If addInVisibleColumn Then
listItem.SubItems.Add(.GetAttribute("text"))
Else
listItem.Text = .GetAttribute("text")
End If
Else
lsv.Items.Add(listItem)
listItem = New ListViewItem
End If
End If
If .Name = "subItem" Then
listItem.SubItems.Add(.GetAttribute("text"))
End If
Loop
.Close()
End With
For i As Integer = 0 To lsv.Groups.Count - 1
lsv.Groups(i).Name = lsv.Groups(i).Header
Next
Return String.Empty Catch ex As Exception
Return ex.Message End Try
End Function
#End Region
#End Region
Public Shared Function Save(ByVal lsv As ListView, ByVal destinationPath As String) As String
If lsv.Groups.Count > 0 AndAlso lsv.ShowGroups Then
Return ListViewWithGroupsSave(lsv, destinationPath)
Else
Return ListViewWithNoGroupsSave(lsv, destinationPath)
End If
End Function
Public Shared Function Load(ByVal lsv As ListView, ByVal loadedPath As String, Optional ByVal append As Boolean = False,
Optional ByVal loadColumns As Boolean = True, Optional ByVal addInVisibleColumn As Boolean = False) As String
Dim hasFileGroups As Boolean =
Regex.IsMatch(File.ReadAllText(loadedPath).Replace(Chr(10), ""), "(<groups>.*</groups>)|(<groups />)",
RegexOptions.IgnoreCase)
If hasFileGroups Then
Return ListViewWithGroupsLoad(lsv, loadedPath, append, loadColumns, addInVisibleColumn)
Else
Return ListViewWithNoGroupsLoad(lsv, loadedPath, append, loadColumns, addInVisibleColumn)
End If
End Function
End Class
Using the Code
At “We have to identify whether the ListView to be saved has groups or not!”, I said that there have to be four private shared functions
and two public
shared functions
in the class, and the class itself has to be declared as MustInherit
. Now I’m going to explain why.
When you have a class, you usually have to dimension an instance of it to access its functions. But when you declare them as “shared
”, you don’t need
to. And that the class is declared as “MustInherit
” means that no object can be created out of this class and you must use it as base class.
Here some examples how to use the functions:
- Save the content to a file named “config.smp” on the user’s Desktop:
ListViewContent.Save(ListView1, System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "config.smp"))
-
Append the currently saved file and add an invisible column to the ListView:
ListViewContent.Load(ListView1, System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "config.smp"), True, True, True)
-
Append the currently saved file without loading the ColumnHeaders:
ListViewContent.Load(ListView1, System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "config.smp"), True, False)
Sample Application
I have also written a demo application to demonstrate how you can use my ListViewContent-class.
In this program, you can add groups and items to the ListView control. If you choose the item “-none-“ in the ComboBox, then the groups in the ListView
are hidden
(set its property “ShowGroups
” to “False
”).
After loading with the option to add an invisible HeaderColumn, you can check the CheckBox for centering the texts of all HeaderColumns.
History
-
23.02.2012 – First version posted.