views:

63

answers:

4

I am totally new to WPF, I have created a simple WPF app that lists whole drive structure (folder, files) to a TreeView, since this process takes a while I tried to use a thread to run the GetFolderTree() method and prevent the UI from becoming unresponsive, however I am facing some problems, I have created a Class named FolderBrowser where I have all that drive structure gathering code, inside that class I create a new instance of TreeViewItem which holds drive structure at the end it is used as return value to populate the TreeView, This is the code:

using System.IO;
using System.Windows.Controls;

namespace WpfApplication  
{
  public class FolderBrowser  
  {  
    private TreeViewItem folderTree;
    private string rootFolder;

    public FolderBrowser(string path)
    {
        rootFolder = path;
        folderTree = new TreeViewItem();
    }

    private void GetFolders(DirectoryInfo di, TreeViewItem tvi)
    {
        foreach (DirectoryInfo dir in di.GetDirectories())
        {
            TreeViewItem tviDir  = new TreeViewItem() { Header = dir.Name };         

            try
            {
                if (dir.GetDirectories().Length > 0)
                    GetFolders(dir, tviDir);

                tvi.Items.Add(tviDir);
                GetFiles(dir, tviDir);
            }
            //catch code here
        }

        if (rootFolder == di.FullName)
        {
            folderTree.Header = di.Name;
            GetFiles(di, folderTree);
        }
    }

    private void GetFiles(DirectoryInfo di, TreeViewItem tvi)
    {
        foreach (FileInfo file in di.GetFiles())
        {
            tvi.Items.Add(file.Name);
        }
    }

    public TreeViewItem GetFolderTree()
    {
        DirectoryInfo di = new DirectoryInfo(rootFolder);
        if (di.Exists)
        {                
            GetFolders(di, folderTree);                                
        }

        return folderTree;
    }
  }
}

How could I create new control instances inside this new thread?

Thanks in advance

+1  A: 

You can't interact with the UI in any thread but the UI thread, but you can use the UI Dispatcher object to execute a callback inside the UI thread:

System.Windows.Application.Current.Dispatcher.Invoke(new System.Action(() => { /* your UI code here */ }));

A more "clean" way of obtaining the dispatcher is to pass it from the UI object to the thread/class that spawns the thread, when you are creating it.

Edit:

I recommend HCL's solution over mine. However, you asked in comments how to get this to work without duplicating this big nasty block of code everywhere:

In your constructor, take a reference to a Dispatcher object, and store it within your class.

Then make a method like this:

private void RunOnUIThread(Action action)
{
    this.dispatcher.Invoke(action);
}

And call it like this:

RunOnUIThread(() => { /* UI code */ });

You can wrap large blocks of code this way:

RunOnUIThread(() =>
{
  Console.WriteLine("One statement");
  Console.WriteLine("Another statement");
});

If you try to push too much of this code back into the UI, though, it will be no different than if you executed all the code within the UI thread, and will still hang the UI.

However, HCL's suggestion of populating a custom tree structure, instead of having that code know anything about UI controls, is much better :)

Merlyn Morgan-Graham
As far as I know, he must create each TreeViewItem in the UI-Thread. Therefore this solution may be a litte difficult (but possible). However I'm not sure if it is as I think.
HCL
I used your code in GetFolders method Application.Current.Dispatcher.Invoke(new System.Action(() => { tviDir = new TreeViewItem() { Header = dir.Name }; })); and it worked but what about the other places where that tviDir is modified, should I use same Application.Current.Dispatcher.Invoke(...) for each line / code block, is there any syntax that allow me to embrace all the method instead ?
Albert
Oh ok, I wanted to know if there were a way of doing it directly with the UI controls, but seems I better avoid them on this. Thanks
Albert
+2  A: 

If the solution from Merkyn Morgan-Graham does not work (see my comment, I'm not sure), I propose to create a independent object-structure that contains your directory-objects.

Do this work with a BackgroundWorker. If it's finished, use this structure to build either the TreeViewItem-nodes directly (because this is not so slow if you have a few hundred of them) or use it as a ViewModel (better).

BackgroundWorker bgWorker = new BackgroundWorker();
bgWorker.DoWork += (s, e) => {
    // Create here your hierarchy
    // return it via e.Result                
};
bgWorker.RunWorkerCompleted += (s, e) => {
    // Create here your TreeViewItems with the hierarchy from  e.Result                
};
bgWorker.RunWorkerAsync();
HCL
+1, because it is better to have your view and implementation be independent from each other, and use data binding to glue them together (MVVM).
Merlyn Morgan-Graham
I tried your code sample but I am still getting "The calling thread must be STA, because many UI components require this." when it tries to create a new instance of TreeViewItem
Albert
@Albert: I suspect you are trying to create the TreeViewItems in the DoWork-event. As I already wrote, this is as far as I know not possible. Try to create a structure that is not of visual objects (more exactly, of DependencyObjects) as TreeViewItems are, and then in RunWorkerComplete, use this as a source to build your items or as a VM.
HCL
Thank you HCL, I followed your advice and I changed class FolderBrowser above to use custom objects tu create the structure.
Albert
A: 

I would suggest that you look into Hierarchical templates instead of manually building the tree. You could build the entire structure in a background thread and then bind the resulting data structure to your tree.

mdm20
A: 

Here is an answer using MVVM (well, at least the view/view-model part) and background worker threads. This uses the background worker to populate the view model (recursively), and the hierarchical data template to bind the view to the view model.

Note that we still have the same threading problem, because the worker thread cannot change the ObservableCollection. So, we use the RunWorkerCompleted event handler (which executes in the UI thread) to populate the collection.

MainWindow.xaml:

<Window
    x:Class="WpfApplication.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication">
    <StackPanel>
        <TextBlock Text="Contents:" />
        <TreeView ItemsSource="{Binding BaseDirectory.Contents}">
            <TreeView.Resources>
                <HierarchicalDataTemplate
                      DataType="{x:Type local:FileSystemEntry}"
                      ItemsSource="{Binding Contents}">
                    <TextBlock Text="{Binding Name}" />
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>
    </StackPanel>
</Window>

MainWindowViewModel.cs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;

namespace WpfApplication
{
    public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            this.BaseDirectory = new FileSystemEntry("C:\\");
            this.BaseDirectory.Populate();
        }

        public FileSystemEntry BaseDirectory { get; private set; }
    }

    public class FileSystemEntry
    {
        public FileSystemEntry(string path)
            : this(new DirectoryInfo(path))
        {
        }

        private FileSystemEntry(DirectoryInfo di)
            : this()
        {
            this.Name = di.Name;
            this.directoryInfo = di;
        }

        private FileSystemEntry(FileInfo fi)
            : this()
        {
            this.Name = fi.Name;
            this.directoryInfo = null;
        }

        private FileSystemEntry()
        {
            this.contents = new ObservableCollection<FileSystemEntry>();
            this.Contents = new ReadOnlyObservableCollection<FileSystemEntry>(this.contents);
        }

        public string Name { get; private set; }

        public ReadOnlyObservableCollection<FileSystemEntry> Contents { get; private set; }

        public void Populate()
        {
            var bw = new BackgroundWorker();

            bw.DoWork += (s, e) =>
            {
                var result = new List<FileSystemEntry>();

                if (directoryInfo != null && directoryInfo.Exists)
                {
                    try
                    {
                        foreach (FileInfo file in directoryInfo.GetFiles())
                            result.Add(new FileSystemEntry(file));

                        foreach (DirectoryInfo subDirectory in
                            directoryInfo.GetDirectories())
                        {
                            result.Add(new FileSystemEntry(subDirectory));
                        }
                    }
                    catch (UnauthorizedAccessException)
                    {
                        // Skip
                    }
                }

                System.Threading.Thread.Sleep(2000); // Todo: Just for demo purposes

                e.Result = result;
            };

            bw.RunWorkerCompleted += (s, e) =>
            {
                var newContents = (IEnumerable<FileSystemEntry>)e.Result;

                contents.Clear();
                foreach (FileSystemEntry item in newContents)
                    contents.Add(item);

                foreach (FileSystemEntry subItem in newContents)
                    subItem.Populate();
            };

            bw.RunWorkerAsync();
        }

        private ObservableCollection<FileSystemEntry> contents;
        private DirectoryInfo directoryInfo;
    }
}
Merlyn Morgan-Graham