0

My code has a function that scans a root directory and showing txt files (for example) in a TreeView:

private TreeNode DirectoryToTreeView(TreeNode parentNode, string path,
                                     string extension = ".txt")
{
    var result = new TreeNode(parentNode == null ? path : Path.GetFileName(path));
    foreach (var dir in Directory.GetDirectories(path))
    {
        TreeNode node = DirectoryToTreeView(result, dir);
        if (node.Nodes.Count > 0)
        {
            result.Nodes.Add(node);
        }
    }
    foreach (var file in Directory.GetFiles(path))
    {
        if (Path.GetExtension(file).ToLower() == extension.ToLower())
        {
            result.Nodes.Add(Path.GetFileName(file));
        }
    }
    return result;
}

This function should by called from the button like: treeView1.Nodes.Add(DirectoryToTreeView(null, @"C:\Users\Tomer\Desktop\a")); Its obviously freezing the UI. I am new to this and I have searched the web and nothing seemed relevant to my problem because no one used recursive function and I cant simply call BeginInvoke on the entire function because it will have no effect. What path should I take? Maybe change the function to work with a while loop and then calling BeginInvoke inside the if statements? Creating a TreeNode object in memory to populate (which may be too large)?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
duck17
  • 15
  • 3
  • 1
    Assuming WinForms. You can run a Task that fetches the Nodes: the slow part is the Files enumeration. You can then await the `[TreeView].SuspendLayout()`, await the task, e.g.: `[TreeView].Nodes.AddRange(await Task.Run(()=> GetTreeNodes([params])))` and resume the Layout: `[TreeView].ResumeLayout(false);`. Now the TreeView is updated only when the Task returns its results and the UI doesn't freeze. -- Tag the question specifying the GUI framework in use in any case. – Jimi Aug 22 '21 at 07:46
  • Careful with a recursive method. Probably better if you use a intermediate method that returns the results of `DirectoryToTreeView()` (that's why I introduced a `GetTreeNodes([params])` method). – Jimi Aug 22 '21 at 08:01
  • Thanks for answering, mentioned now that its win forms. I did not understand how this solution will work with this recursive function and where should i be calling each of the things you mentioned. – duck17 Aug 22 '21 at 09:01
  • 1
    As mentioned, you can use an intermediate method (I've previously named it `GetTreeNodes()`) that will return the results of your `DirectoryToTreeView()`. -- You can execute the code described in the first comment from any async method. It could be the handler of the `Click` event of a Button, but you can use `Load` event handler of your Form or override `OnLoad()`) etc. If the calling method is not an event handler, it must be a method that returns `Task` (or `Task`, depending on its role / implementation). Not a `void` method. – Jimi Aug 22 '21 at 09:28

2 Answers2

1

Here's an example for async Task method to populate a TreeNode with a directory-tree for a given file type. The inner CreateTree(...) is a local function called recursively to traverse the directories.

private async Task<TreeNode> CreateTreeAsync(string startDir, string fileExt)
{
    var di = new DirectoryInfo(startDir);
    var result = new TreeNode(di.Name);
    var searchPattern = $"*.{fileExt.TrimStart('.')}";

    return await Task.Run(() =>
    {
        void CreateTree(DirectoryInfo dirInfo, TreeNode node)
        {
            try
            {
                foreach (var fileInfo in dirInfo.EnumerateFiles(searchPattern))
                    node.Nodes.Add(fileInfo.Name);

                foreach (var subDir in dirInfo.EnumerateDirectories())
                {
                    try
                    {
                        // Optional to skip the branches with no files at any level.
                        if (!subDir.EnumerateFiles(searchPattren, 
                            SearchOption.AllDirectories).Any()) continue;

                        var newNode = new TreeNode(subDir.Name);
                        node.Nodes.Add(newNode);
                        CreateTree(subDir, newNode);
                    }
                    catch (Exception ex)
                    {
                        // Skip exceptions like UnauthorizedAccessException
                        // and continue...
                        Console.WriteLine(ex.Message);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        CreateTree(di, result);
        return result;
    });
}

Note: You don't need the try..catch blocks if you're under .NET 5+/.NET Core to skip the inaccessible directories and files. Use the EnumerateXXX method overload that takes the EnumerationOptions parameter.

Now you need async caller like so:

private async void someButton_Click(object sender, EventArgs e)
{        
    // Optional...                
    treeView1.Nodes.Clear();

    var dir = @"...";
    var ext = "txt";
    var node = await CreateTreeAsync(dir, ext);

    if (node.Nodes.Count == 0)
        MessageBox.Show($"No '{ext}' files were found.");
    else
    {
        treeView1.Nodes.Add(node);
        node.Expand();
    }
}
dr.null
  • 4,032
  • 3
  • 9
  • 12
1

You could convert the DirectoryToTreeNode method to an asynchronous method, and offload any blocking I/O operation to the ThreadPool, by using the Task.Run method:

private async Task<TreeNode> DirectoryToTreeNodeAsync(string path,
    TreeNode parentNode = null)
{
    var node = new TreeNode(parentNode == null ? path : Path.GetFileName(path));
    string[] subdirectories = await Task.Run(() => Directory.GetDirectories(path));
    foreach (string dirPath in subdirectories)
    {
        TreeNode childNode = await DirectoryToTreeNodeAsync(dirPath, node);
        node.Nodes.Add(childNode);
    }
    string[] files = await Task.Run(() => Directory.GetFiles(path));
    foreach (string filePath in files)
    {
        node.Nodes.Add(Path.GetFileName(filePath));
    }
    return node;
}

Notice that no UI control is touched while running on the ThreadPool (inside the Task.Run delegate). All UI controls should be manipulated exclusively by the UI thread.

Usage example:

private async void Button1_Click(object sender, EventArgs e)
{
    Button1.Enabled = false;
    Cursor = Cursors.WaitCursor;
    try
    {
        TreeView1.Nodes.Clear();
        TreeView1.Nodes.Add(
            await DirectoryToTreeNodeAsync(@"C:\Users\Tomer\Desktop\a"));
    }
    finally
    {
        Cursor = Cursors.Default;
        Button1.Enabled = true;
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • For a comparison between the `BackgroundWorker` and the `Task.Run` + async/await, you could take a look at [this](https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker/64620920#64620920 "Async/await vs BackgroundWorker") answer. – Theodor Zoulias Aug 22 '21 at 16:59
  • This doesn't work. 1) You should skip the security and IO exceptions in the recursive method. Using `try..catch` in the caller won't continue the progress. The iteration breaks with the first exception. 2) You're getting everything not just the required file type (the `.txt` files). 3) Why are you calling `.GetDirectories` and `.GetFiles`? [Read](https://stackoverflow.com/questions/5669617/what-is-the-difference-between-directory-enumeratefiles-vs-directory-getfiles). – dr.null Aug 23 '21 at 07:14
  • @dr.null this is a minimal example that demonstrates a technique. I removed the filter for `.txt` files and the check for empty folders, as redundant complexity that obfuscates the core problem. I am sure that the OP will be able to add it back. As for the `.GetDirectories` and `.GetFiles`, this is what the OP uses in the question. I think that the `GetFiles` is superior to the `EnumerateFiles` in this specific case, because it minimizes the number of jumps between the UI thread and the `ThreadPool`. – Theodor Zoulias Aug 23 '21 at 07:29
  • 1
    Endedup using this solution, was easier to implement in my code and was slightly faster, thanks a lot. – duck17 Aug 30 '21 at 04:59