Windows Forms in C#: Working with Directory

1. Finding Directory Sizes

The base of DiskDiff is the code that can traverse a directory tree and figure out the sizes of the files there.

A quick look in the System.IO namespace shows a DirectoryInfo class that will work. You’ll encapsulate it in a class of your own so you can store the list of files and the list of directories in each directory. The class will look like this:

public class DirectoryNode {

string root;

List<FileNode> files = new List<FileNode>();

List<DirectoryNode> dirs = new List<DirectoryNode>();

DirectoryInfo directoryInfo; // this directory

public DirectoryNode(string root)

{

this.root = root;

directoryInfo = new DirectoryInfo(root);

}

public DirectoryNode(DirectoryInfo directoryInfo)

{

this.directory = directoryInfo;

this.root = directoryInfo.FullName;

}

public void Populate()

{

foreach (FileInfo f in directoryInfo.GetFiles())

{

FileNode fileNode = new FileNode(f);

this.files.Add(f);

}

foreach (DirectoryInfo d in directory.GetDirectories())

{

DirectoryNode dirNode = new DirectoryNode(d.FullName);

dirs.Add(dirNode);

dirNode.Populate();

}

}

public void PrintTree(int level)

{

for (int i = 0; i < level; i++)

{

Console.Write(” “);

}

Console.WriteLine(“{0}”, this.root);

foreach (DirectoryNode dirNode in dirs)

{

dirNode.PrintTree(level + 1);

}

}

}

DirectoryNode has two constructors. The first creates the top-level directory, and the other creates a DirectoryNode from a DirectoryInfo object.

The Populate() function is the heart of the class. It uses the DirectoryInfo object that’s encapsulated, calling GetFiles() to get the list of files in the directory and GetDirectories() to get the list of directories. It then recurses for each subdirectory, so the directory tree can be fully traversed.

The PrintTree() function is used for testing, along with a little test program:

class Test {

public static void Main()

{

DirectoryNode directoryNode = new DirectoryNode(@”c:\project\diskdiff”);

directoryNode.Populate();

directoryNode.PrintTree(0);

}

}

On our system, this gives the following output:

c:\project\diskdiff

c:\project\diskdiff\bin

c:\project\diskdiff\bin\Debug

c:\project\diskdiff\obj

c:\project\diskdiff\obj\Debug

c:\project\diskdiff\obj\Debug\temp

c:\project\diskdiff\obj\Debug\TempPE

2. Calculating Sizes

Now that you have the directory code, you need to sum the sizes of the files for a directory and then pass that size up to the parent directories. You’ll want to be able to fetch both the size of this directory and the size of this directory and the subdirectories under it. For this, you’ll add two properties:

long? size = null;                                // size of dir in bytes

long? sizeTree = null;                         // size of dir and subdirs

public long Size {

get

{

if (size == null)

{

size = 0;

foreach (FileNode f in files)

{

size += f.fileInfo.Length;

}

}

return(size);

}

}

public long SizeTree {

get

{

if (sizeTree == null)

{

sizeTree = 0; sizeTree += Size;

foreach (DirectoryNode dirNode in dirs) {

sizeTree += dirNode.SizeTree;

}

}

return(sizeTree);

}

}

The Size property simply walks through all the files in the current node and adds their sizes. This total size is then stored in the size variable so it doesn’t need to be recalculated.

The SizeTree property adds the size of all the subdirectories to the current size. Because SizeTree recourses down the tree, getting the value of the property at the root will cause the sizes to be calculated all the way down the tree.

Although this is a nice way to use properties, it may turn out that calculating the sizes during the call to Populate() is a better choice.

The PrintTree() is renamed to PrintSizes() and now prints out the values of Tree and TreeSize next to the name of the directory. Running the code produces the following output:

c:\project\diskdiff 32632 119672

c:\project\diskdiff\bin 0 43520

c:\project\diskdiff\bin\Debug 43520 43520

c:\project\diskdiff\obj 0 43520

c:\project\diskdiff\obj\Debug 43520 43520

c:\project\diskdiff\obj\Debug\temp 0 0

c:\project\diskdiff\obj\Debug\TempPE 0 0

Now it’s time to integrate the directory traversal code into the TreeView class.

3. A Debugging Suggestion

Though a separate DirectoryNode.cs file is part of the example, the class was written in the main project. For testing purposes, we wanted to print the traversal of a big directory, but since DiskDiff is a Windows Forms project, it doesn’t have a console window.

However, a Windows Forms project can have a console window. By right-clicking the project in the Solution Explorer and choosing Properties, you can change the output type of the project to Console Application.

This means you’ll have to dismiss the console window when exiting the application, but that’s fine for debugging. When you’re finished, just change the output type of the project back, and the console window will go away.

4. Displaying the Directory Tree and Sizes

You must now decide how to interface the form and the DirectoryNode classes so the tree can be populated. One option is to have DirectoryNode expose enough of its internals (perhaps through an indexer) so the form can iterate over it. Another option is to pass the TreeView control to a function in DirectoryNode and have it populate the control.

A trade-off exists between having DirectoryNode expose a more complex interface and having DirectoryNode be more tightly coupled to the form. In this case, we’ll choose the first option, but the second option can sometimes be better, especially if the processing to be done is complex.

Because a directory can be thought of as an array of files, an indexer is a reasonable choice. However, you need to differentiate between directories and files, so doing what the Directory class does, with GetFiles() and GetDirectories() members, is a better choice. Rather than return the collection held by DirectoryNode, a copy of the collection is made, sorted, and returned. This allows the UI to make any modifications it needs to the collection without affecting other objects that might be using the same DirectoryNode instance.

The code for these functions is as follows:

public DirectoryNode[] GetDirectories()

{

DirectoryNode[] array = dirs.ToArray();

Array.Sort(array);

return (array);

}

public FileNode[] GetFiles()

{

FileNode[] array = files.ToArray();

Array.Sort(array);

return (array);

}

This is a good example of how garbage collection can simplify interaction between different objects; in C++, you’d have to carefully consider the question of who owned the objects in the returned array.

The code to populate the TreeView object is only a few lines:

public void PopulateTree(TreeNodeCollection treeNodeCollection,

DirectoryNode directoryNode)

{

TreeNode treeNode = new TreeNode(directoryNode.NameSize);

treeNodeCollection.Add(treeNode);

foreach (DirectoryNode subdir in directoryNode.GetDirectories())

{

PopulateTree(treeNode.Nodes, subdir);

}

foreach (FileNode fileNode in directoryNode.GetFiles())

{

TreeNode treeFileNode = new TreeNode(fileNode.NameSize);

treeNode.Nodes.Add(treeFileNode);

}

}

This is a recursive function. It adds a TreeNode instance to the collection for the current level and then recurses to add all the subdirectories under this directory as children of this directory.

It then adds entries for all the FileNode objects in this directory. The FileNode object was added to encapsulate functions dealing with files—primarily the NameSize property that returns the properly formatted string for this file.

When this code is run, it generates the window (after expanding all the nodes) shown in Figure 35-3.

5. Setting the Directory

The FolderBrowserDialog class allows a directory to be selected in a standard Windows manner, as shown in Figure 35-4.

The following is the code to display and use FolderBrowserDialog:

FolderBrowserDialog dialog = new FolderBrowserDialog();

dialog.ShowNewFolderButton = false;

if (dialog.ShowDialog() == DialogResult.OK)

{

directoryNodeBaseline = null;

rootDirectory = dialog.SelectedPath;

DoTree();

}

This code has a slight problem, however. If the root directory c:\ is entered, it will take the code quite a while to fetch all the information, and the program will appear to hang. That’s bad.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *