Contents
The code in the accompanying demo project includes all the routines needed to add file system treeview and listview controls to a VB.NET project. The demo project illustrates how to set up a treeview and listview control that provides access to the local file system similar to Windows Explorer.
In the project I'm currently working on, I have to include an FTP window. To do so, I first created a class that encapsulates all FTP functionality. Then I proceeded to design a user interface to front the FTP class. This user interface has three sections. One is for navigating and manipulating the local file system. Another is for connecting to a remote FTP server. The third is for navigating and manipulating the remote file system. While putting together the sections for connecting to and navigating the remote machine proved fairly simple, exposing the file system on the local machine proved to be surprisingly difficult.
I was not aware that, unlike VB 6, there are no file system controls in VB.NET. I soon also discovered that, short of paying a premium for such controls, I could not find comprehensive sample code on the Internet. After successfully cobbling together the needed code, I thought there might be others who would appreciate finding a complete set of code for VB.NET file system controls.
There are a few classes and methods in the demo project that are from my toolkit project. Including the entire toolkit would prove unwieldy and confusing. Hence, I have included in the demo project only those classes and methods needed to support the demo project code.
There are three classes in the demo project that came from my toolkit.
C_FileExists
... In addition to checking whether a file exists, this class includes methods for deleting and renaming a file.
C_FolderExists
... In addition to checking whether a folder exists, this class includes methods for creating new paths, deleting a folder including all its contents, and renaming a folder.
C_StringBuilder
... The .NET Stringbuilder encapsulated in my toolkit so that I can use it without having to import the System.Text
namespace.
Then there are methods from my C_Toolkit
class that have been made available in the demo project M_Common
module. These transplanted methods include, but are not limited to the following.
AddChar
... If not present, appends a character to the passed string. The default character is a backslash.
ByteUnits
... Converts a total number of bytes into its highest units (Bytes, KB, MB, GB)
FilenameFromPath
... An overloaded method that extracts the filename from a full path.
NewName
... Asks the user for a new folder or file name. Validates that the name does not include invalid characters.
RemoveTrailingCharacter
... Removes one or more occurrences of one or more trailing characters from a string.
ValidFileName
... Tests a file (or folder) name for invalid characters.
The demo project includes one of my extended user controls; the extended textbox. You can read about this useful control in this article:
VB.NET User Controls: Extended and Date Textboxes
As you can see in the demo window above, the code provided in the demo project includes full support for a label that displays the path for the currently selected folder, a treeview for displaying and navigating the file system structure, plus a listview that shows the files in the currently selected folder. Using the buttons, the user can add folders, rename folders and files, delete files as well as folders plus their entire contents, and refresh the controls. When the Refresh button is clicked, the treeview is repopulated and the originally selected node is reselected and made visible regardless of how deep the originally selected node appears in the file system tree.
In this article I will not attempt to describe the entire demo project. Instead, I will highlight that code which performs significant functions within the project. First, a note about imported namespaces. Aside from the three classes included in the demo project from my toolkit project, there is only one Imports
statement which is in the main demo project form.
Imports System.IO
For reference purposes, the controls in the demo project have these names.
lblLocalPath
... a label that displays the full path for the currently selected treeview node
tvwLocalFolders
... a treeview that displays the file system structure
lvwLocalFiles
... a listview that shows the files in the currently selected folder
btnLocalAddDir
... the Add Dir button
btnLocalRename
... the Rename button
btnLocalDelete
... the Delete button
btnLocalRefresh
... the Refresh button
As to why the pervasive use of "Local" in the control names ... remember that this code is from an FTP window which has like named "Remote" controls.
At application startup, the ListRootNodes
and ListLocalSubFolders
methods handle the chore of initializing the treeview control. When the ListRootNodes
method is invoked from within the Form Load
event, a root node for each drive in the local file system is added to the treeview. In addition, the first layer of folders below each drive node is also added so that, at startup, the treeview includes "plus" buttons for each drive that has subfolders.
Private Sub ListRootNodes()
Dim nodeText As String = ""
Dim sb As New C_StringBuilder
tvwLocalFolders.Nodes.Clear()
tvwLocalFolders.BeginUpdate()
With My.Computer.FileSystem
For i As Integer = 0 To .Drives.Count - 1
sb.ClearText()
sb.AppendText(.Drives(i).DriveType.ToString)
sb.AppendText(gtSPACE)
sb.AppendText(gtLEFT_PAREN)
sb.AppendText(RemoveTrailingChar(.Drives(i).Name, gtBACKSLASH))
sb.AppendText(gtRIGHT_PAREN)
nodeText = sb.FullText
Dim driveNode As TreeNode
driveNode = tvwLocalFolders.Nodes.Add(nodeText)
driveNode.ImageIndex = 0
driveNode.SelectedImageIndex = 1
driveNode.Tag = .Drives(i).Name
Try
ListLocalSubFolders(driveNode, .Drives(i).Name)
Catch ex As Exception
End Try
driveNode = Nothing
Next
End With
tvwLocalFolders.EndUpdate()
End Sub
After each drive root node is added in the ListRootNodes
method, the ListLocalSubFolders
method is invoked to insert any folders immediately below the drive root node just added.
Private Sub ListLocalSubFolders(ByVal ParentNode As TreeNode, _
ByVal ParentPath As String)
Dim FolderNode As String = ""
Try
For Each FolderNode In Directory.GetDirectories(ParentPath)
Dim childNode As TreeNode
childNode = ParentNode.Nodes.Add(FilenameFromPath(FolderNode))
With childNode
.ImageIndex = 0
.SelectedImageIndex = 1
.Tag = FolderNode
End With
childNode = Nothing
Next
Catch ex As Exception
End Try
End Sub
Note that in both the ListRootNodes
and ListLocalSubFolders
methods the Tag
property for each treeview node is set to the full directory path for that node. Having the full path available for each node is key to making all of the other methods in the demo project work.
The listview shows all of the files in the folder that is selected in the treeview. The list of files is updated, along with the displayed path for the currently selected node, each time a node is selected and the tvwLocalFolders.AfterSelect
event fires. This is the first example of where the full directory paths stored in each node's Tag
property is put to use.
In this and other methods there is a test to determine whether the method is to be ignored. Each of these tests prevents unwanted methods from being executed when the processing launched by the Refresh button is triggered.
Private Sub tvwLocalFolders_AfterSelect( _
ByVal sender As Object, _
ByVal e As _
System.Windows.Forms.TreeViewEventArgs) _
Handles tvwLocalFolders.AfterSelect
If fbIgnoreClick Then
Exit Sub
End If
TurnOnHourglass()
Dim folder As String = tvwLocalFolders.SelectedNode.Tag
lblLocalPath.Text = folder
ListLocalFiles(folder)
TurnOnArrow()
End Sub
Within the tvwLocalFolders_AfterSelect
method, the files listview is populated by invoking the ListLocalFiles
method.
Private Sub ListLocalFiles(ByVal ParentPath As String)
lvwLocalFiles.Items.Clear()
lvwLocalFiles.BeginUpdate()
Try
For Each filePath As String In Directory.GetFiles(ParentPath)
Select Case _
(File.GetAttributes(filePath) And FileAttributes.Hidden)
Case FileAttributes.Hidden
Case Else
Dim lvi As New ListViewItem
lvi.SubItems(0).Text = FilenameFromPath(filePath)
lvi.SubItems.Add(File.GetLastAccessTime(filePath))
lvi.SubItems.Add(ByteUnits( _
CType(New FileInfo(filePath).Length, Integer)))
lvi.Tag = filePath
lvwLocalFiles.Items.Add(lvi)
lvi = Nothing
End Select
Next
Catch ex As Exception
End Try
lvwLocalFiles.EndUpdate()
End Sub
Since the Directory.GetFiles
method does not discriminate between hidden and non-hidden files, the FileAttributes
property of each file is examined to weed out the hidden files.
Folders can be added to the local file system with the Add Dir button. When the Add Dir button is clicked, a dialog box is displayed from the btnLocalAddDir.Click
event that queries the user for the name of a folder to add below the currently selected treeview node.

The dialog box is displayed by the NewName
method which ensures the new folder name does not contain any invalid characters. If the user enters a folder name that contains invalid characters, an error message is displayed by the ValidFilename
method.
Public Function NewName(ByVal InputBoxMsg As String, _
ByVal InputBoxTitle As String) As String
Dim name As String = ""
Do
name = InputBox(InputBoxMsg, InputBoxTitle, name)
If name.Length <= 0 Then
Exit Do
End If
Loop While Not ValidFilename(name)
Return name
End Function
Once a valid folder name has been entered, the remainder of the btnLocalAddDir.Click
event adds the new folder and updates the treeview.
With sb
.ClearText()
.AppendText(AddChar(lblLocalPath.Text, gtBACKSLASH))
.AppendText(newFolder)
newFolder = .FullText
End With
sb = Nothing
Dim nf As New C_FolderExists(newFolder)
If nf.FolderExists Then
DisplayOops("That folder already exists", False)
lblLocalPath.Focus()
Exit Sub
End If
If Not nf.CreatePath Then
DisplayOops(nf.CreatePathError, False, "Error")
lblLocalPath.Focus()
Exit Sub
End If
TurnOnHourglass()
RefreshLocalFolders( _
tvwLocalFolders.SelectedNode, _
tvwLocalFolders.SelectedNode.Tag, _
tvwLocalFolders.SelectedNode.IsExpanded)
TurnOnArrow()
lblLocalPath.Focus()
The RefreshLocalFolders
method updates the treeview so that the new folder is displayed.
Private Sub RefreshLocalFolders( _
ByVal SelectedNode As TreeNode, _
ByVal SelectedNodeTag As String, _
ByVal SelectedNodeExpanded As Boolean)
tvwLocalFolders.BeginUpdate
Dim deleteNode As TreeNode = SelectedNode.FirstNode
Do Until deleteNode Is Nothing
deleteNode.Remove()
deleteNode = SelectedNode.FirstNode
Loop
ListLocalSubFolders(SelectedNode, _
SelectedNodeTag)
If SelectedNodeExpanded Then
Dim childNode As TreeNode = SelectedNode.FirstNode
Do Until childNode Is Nothing
ListLocalSubFolders(childNode, _
childNode.Tag)
childNode = childNode.NextNode
Loop
End If
tvwLocalFolders.EndUpdate
End Sub
Both folders and files can be renamed by clicking the Rename button. What happens when the Rename button is clicked depends on whether any files are selected in the listview, and whether the node selected in the treeview is a root node.
- If one or more files are selected, new names for those files are requested from the user first. Then, assuming a root node is not selected, the user is queried for a new folder name for the selected node.
- If no files are selected, and the selected node is not a root node, a new name for the selected folder is requested.
- If no files are selected, and the selected node is a root node, a
Beep
is sounded.

The same Rename dialog box is used for querying the user for new folder and file names. Only the Folder or File section in the dialog box is active when the window is displayed. Which section is active is controlled from within the btnLocalRename.Click
event with a file path placed in one of two global post office box variables located in the M_Common
module (gtPOBox01
or gtPOBox02
).
Before entering the file renaming the For...Next
loop in the btnLocalRename.Click
event, all of the paths for the selected files are captured in an array. This is necessary because, as the files are renamed and the listview updated, the sequence in which the files are listed will probably change.
Private Sub btnLocalRename_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnLocalRename.Click
If lblLocalPath.Text.Length <= 0 Then
Beep()
lblLocalPath.Focus()
Exit Sub
End If
If lvwLocalFiles.SelectedItems.Count > 0 Then
gtPOBox01 = ""
Dim filePaths() As String
ReDim filePaths(lvwLocalFiles.SelectedItems.Count - 1)
For i As Integer = 0 To filePaths.GetUpperBound(0)
filePaths(i) = lvwLocalFiles.SelectedItems(i).Tag
Next
For j As Integer = 0 To filePaths.GetUpperBound(0)
gtPOBox02 = filePaths(j)
Dim re As New F_Rename
Select Case re.ShowDialog
Case Windows.Forms.DialogResult.OK
Try
RenameLocalFile(filePaths(j), gtPOBox02)
Catch ex As Exception
DisplayOops(ex.Message, _
False, _
"Error")
lblLocalPath.Focus()
Exit Sub
End Try
Case Windows.Forms.DialogResult.Ignore
Case Windows.Forms.DialogResult.Cancel
lblLocalPath.Focus()
Exit Sub
End Select
re = Nothing
Next
ReDim filePaths(-1)
End If
If lblLocalPath.Text.Length > 3 Then
gtPOBox02 = ""
gtPOBox01 = tvwLocalFolders.SelectedNode.Tag
Dim re As New F_Rename
If re.ShowDialog = Windows.Forms.DialogResult.OK Then
RenameLocalFolder(gtPOBox01)
End If
re = Nothing
End If
lblLocalPath.Focus()
End Sub
The RenameLocalFile
and RenameLocalFolder
methods do the actual renaming chores.
Private Sub RenameLocalFile(ByVal FilePath As String, _
ByVal NewFileName As String)
Dim sf As New C_FileExists(FilePath)
Try
sf.RenameFile(NewFileName)
Catch ex As Exception
Throw New ApplicationException(ex.Message)
End Try
sf = Nothing
Try
ListLocalFiles(tvwLocalFolders.SelectedNode.Tag)
Catch ex As Exception
End Try
End Sub
Private Sub RenameLocalFolder(ByVal NewFolderName As String)
Dim sf As New C_FolderExists(lblLocalPath.Text)
Try
sf.RenameFolder(NewFolderName)
Catch ex As Exception
DisplayOops(ex.Message, False, "Error")
Exit Sub
End Try
sf = Nothing
RefreshLocalFolders( _
tvwLocalFolders.SelectedNode.Parent, _
tvwLocalFolders.SelectedNode.Parent.Tag, _
tvwLocalFolders.SelectedNode.Parent.IsExpanded)
End Sub
The Delete button enables the user to delete both folders and files from the local file system. Like the Rename button, what happens when the Delete button is clicked depends on whether any files are selected, and whether a root node is selected in the treeview.
- If one or more files are selected, those files are deleted first. Then, assuming a root node is not selected, the selected folder is deleted.
- If no files are selected, and the selected node is not a root node, the selected folder is deleted.
- If no files are selected, and the selected node is a root node, a
Beep
is sounded.
Of course, nothing is deleted without first getting an OK from the user. Unlike with the Rename button, a different dialog box is displayed to confirm deletes for files and folders. This arrangement is used so that a Delete All option can be provided when deleting files. Such an option is not relevant when deleting just one folder.

The Delete File dialog box is displayed in turn for each file being deleted unless the user clicks the Delete All button. At that point, the remainder of the selected files, starting with the one displayed in the Delete File dialog box, are deleted without first displaying the dialog box.

Another reason to have separate dialog boxes for files and folders is to stress the point that when the selected folder is deleted, all of the folder's contents are also removed.
The entire file and folder delete process is controlled within the btnLocalDelete.Click
event. As with the Rename process, the paths for all of the files being deleted are first captured so that the removal of deleted files from the listview will not interfer with the For...Next
loop that controls the deletion of files.
Private Sub btnLocalDelete_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnLocalDelete.Click
If lblLocalPath.Text.Length <= 0 Then
Beep()
lblLocalPath.Focus()
Exit Sub
End If
If lvwLocalFiles.SelectedItems.Count > 0 Then
Dim filePaths() As String
ReDim filePaths(lvwLocalFiles.SelectedItems.Count - 1)
For i As Integer = 0 To filePaths.GetUpperBound(0)
filePaths(i) = lvwLocalFiles.SelectedItems(i).Tag
Next
For j As Integer = 0 To filePaths.GetUpperBound(0)
gtPOBox02 = filePaths(j)
Dim de As New F_Delete
Select Case de.ShowDialog
Case Windows.Forms.DialogResult.OK
TurnOnHourglass()
If Not DeleteLocalFile(filePaths(j), _
tvwLocalFolders.SelectedNode.Tag) Then
lblLocalPath.Focus()
Exit Sub
End If
TurnOnArrow()
Case Windows.Forms.DialogResult.Ignore
TurnOnHourglass()
For k As Integer = j To filePaths.GetUpperBound(0)
If Not DeleteLocalFile(filePaths(k), _
tvwLocalFolders.SelectedNode.Tag) Then
lblLocalPath.Focus()
Exit Sub
End If
Next
TurnOnArrow()
Exit For
Case Windows.Forms.DialogResult.Cancel
lblLocalPath.Focus()
Exit Sub
End Select
de = Nothing
Next
ReDim filePaths(-1)
End If
If lblLocalPath.Text.Length > 3 Then
Dim sb As New C_StringBuilder
With sb
.AppendText("This will delete folder ")
.AppendText(lblLocalPath.Text)
.AppendText(" and all of its contents")
.AppendText(ControlChars.CrLf)
.AppendText(ControlChars.CrLf)
.AppendText("Continue with delete?")
gtMsg = .FullText
End With
sb = Nothing
If MessageBox.Show(gtMsg, _
"Delete Folder", _
MessageBoxButtons.YesNo, _
MessageBoxIcon.Question, _
MessageBoxDefaultButton.Button2) = _
Windows.Forms.DialogResult.Yes Then
TurnOnHourglass()
DeleteLocalFolder()
TurnOnArrow()
End If
End If
lblLocalPath.Focus()
End Sub
The actual deleting of files and folders is handled by the DeleteLocalFile
and DeleteLocalFolder
methods.
Private Function DeleteLocalFile(ByVal FilePath As String, _
ByVal NodePath As String) As Boolean
Try
File.Delete(FilePath)
ListLocalFiles(NodePath)
Return True
Catch ex As Exception
DisplayOops(ex.Message, False, "Delete Error")
Return False
End Try
End Function
Private Sub DeleteLocalFolder()
Dim selectedNodeParent As TreeNode = tvwLocalFolders.SelectedNode.Parent
Dim selectedNodeParentTag As String = tvwLocalFolders.SelectedNode.Parent.Tag
Dim selectedNodeParentIsExpanded As Boolean = _
tvwLocalFolders.SelectedNode.Parent.IsExpanded
Dim sf As New C_FolderExists(lblLocalPath.Text)
Try
sf.DeleteFolder()
Catch ex As Exception
DisplayOops(ex.Message, False, "Error")
Exit Sub
End Try
sf = Nothing
RefreshLocalFolders(selectedNodeParent, _
selectedNodeParentTag, _
selectedNodeParentIsExpanded)
End Sub
In my opinion, the code that accomplishes the refreshing of the treeview control is the most interesting code in the demo project. Here's the psuedo code for doing what, at first blush, seemed like it should be a simple, straight-forward task.
- Build an array of node directory paths starting with the currently selected node and then working back up to the parent root node. (Another instance of using the paths stored in each node's
Tag
property.)
- Clear both the treeview and listview.
- Initialize the treeview with drive root nodes and all child nodes at the next level down under each root node. (Same process as performed at application startup.)
- Using the last element in the array of node directory paths from step 1, find the root node that starts the branch down to the originally selected node.
- Using the remainder of the array of node directory paths from step 1 (in reverse order), re-expand the branch of nodes from the original root node back down to the originally selected node.
- Ensure the originally selected node is visible in the treeview.
- Relist any files in the originally selected folder.
- Display the path for the selected node.
The process of re-expanding the treeview down to the originally selected node (steps 4 and 5) has to be robust enough to handle a broken chain. In other words, if a folder in the original branch of selected nodes is deleted or renamed in some other application before the Refresh button is clicked, the re-expansion process has to be able to stop at the point where the branch is broken without throwing an exception.
Here's the btnLocalRefresh.Click
event where all the above steps get done. This was certainly the most challenging process in the demo project to write.
Private Sub btnLocalRefresh_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnLocalRefresh.Click
If lblLocalPath.Text.Length <= 0 Then
Beep()
lblLocalPath.Focus()
Exit Sub
End If
TurnOnHourglass()
Dim branchPaths() As String
Dim i As Integer = -1
Do
i += 1
ReDim Preserve branchPaths(i)
branchPaths(i) = tvwLocalFolders.SelectedNode.Tag
tvwLocalFolders.SelectedNode = tvwLocalFolders.SelectedNode.Parent
Loop While tvwLocalFolders.SelectedNode IsNot Nothing
lvwLocalFiles.Items.Clear()
lvwLocalFiles.Refresh()
tvwLocalFolders.Nodes.Clear()
tvwLocalFolders.Refresh()
ListRootNodes()
Dim foundNode As Boolean = False
For j As Integer = 0 To tvwLocalFolders.Nodes.Count - 1
If tvwLocalFolders.Nodes(j).Tag = branchPaths(i) Then
tvwLocalFolders.SelectedNode = tvwLocalFolders.Nodes(j)
foundNode = True
Exit For
End If
Next
If Not foundNode Then
tvwLocalFolders.Focus()
TurnOnArrow()
Exit Sub
End If
tvwLocalFolders.BeginUpdate()
fbIgnoreClick = True
i -= 1
Do While i > -1
Dim child As TreeNode = tvwLocalFolders.SelectedNode.FirstNode
foundNode = False
Do
If child.Tag = branchPaths(i) Then
tvwLocalFolders.SelectedNode = child
ListLocalSubFolders(tvwLocalFolders.SelectedNode, _
tvwLocalFolders.SelectedNode.Tag)
tvwLocalFolders.SelectedNode.Expand()
foundNode = True
Exit Do
End If
child = child.NextNode
Loop While child IsNot Nothing
If foundNode Then
child = Nothing
Else
Exit Do
End If
i -= 1
Loop
fbIgnoreClick = False
tvwLocalFolders.EndUpdate()
If foundNode Then
ListLocalFiles(tvwLocalFolders.SelectedNode.Tag)
tvwLocalFolders.SelectedNode.EnsureVisible()
End If
Try
lblLocalPath.Text = tvwLocalFolders.SelectedNode.Tag
Catch ex As Exception
End Try
tvwLocalFolders.Focus()
TurnOnArrow()
End Sub
When initially populating the file system treeview, I found that it's not wise to try to populate all node levels. In other words, when first populating the treeview, don't try to populate it with the entire file system. Doing so incurs way too much overhead. Instead, only the nodes at the next level below each expanded node are populated at the time each node is expanded. This limits the overhead for adding nodes to only those that are needed when they are needed.
This selective population of treeview nodes is accomplished by the ListLocalSubFolders
method (see Initializing The Treeview section above) which is called when the tvwLocalFolders.BeforeExpand
event is triggered.
Private Sub tvwLocalFolders_BeforeExpand( _
ByVal sender As Object, _
ByVal e As _
System.Windows.Forms.TreeViewCancelEventArgs) _
Handles tvwLocalFolders.BeforeExpand
If fbIgnoreClick Then
Exit Sub
End If
TurnOnHourglass()
lblLocalPath.Text = e.Node.Tag
Dim parentPath As String = AddChar(e.Node.Tag)
tvwLocalFolders.BeginUpdate()
Dim childNode As TreeNode = e.Node.FirstNode
Do While childNode IsNot Nothing
ListLocalSubFolders(childNode, _
parentPath & childNode.Text)
childNode = childNode.NextNode
Loop
tvwLocalFolders.EndUpdate()
tvwLocalFolders.SelectedNode = e.Node
ListLocalFiles(parentPath)
TurnOnArrow()
End Sub
Writing the code in this project certainly helped me to better understand how the .NET treeview control functions. Now that the project is done and published, I hope others will take this opportunity to get more comfortable with the treeview control. I also hope that readers find the code in this project useful.
11/2008 ... Original article