使用多线程在简单的WPF应用程序中出现问题

3

我完全不了解WPF,我创建了一个简单的WPF应用程序,将整个驱动器结构(文件夹,文件)列在TreeView中。由于这个过程需要一段时间,我尝试使用线程来运行GetFolderTree()方法并防止UI变得无响应,但是我遇到了一些问题。我创建了一个名为FolderBrowser的类,在其中包含所有驱动器结构收集代码,在该类中创建了一个新的TreeViewItem实例,它保存驱动器结构,并最终用作返回值来填充TreeView。以下是代码:

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;
    }
  }
}

我该如何在这个新线程中创建新的控件实例?
提前感谢。

使用XAML/MVVM处理一个小样本...几分钟后会发布新答案。 - Merlyn Morgan-Graham
4个回答

2
如果Merkyn Morgan-Graham的解决方案不起作用(请参见我的评论,我不确定),我建议创建一个独立的对象结构,其中包含您的目录对象。
使用BackgroundWorker完成此工作。如果它完成了,直接使用这个结构来构建TreeViewItem节点(因为如果你只有几百个节点,这不会很慢),或者将其用作ViewModel(更好)。
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();

+1,因为最好让您的视图和实现相互独立,并使用数据绑定将它们粘合在一起(MVVM)。 - Merlyn Morgan-Graham
我尝试了你的代码示例,但当它尝试创建TreeViewItem的新实例时,仍然出现了“调用线程必须是STA,因为许多UI组件需要此项。” - Albert
@Albert:我怀疑你正在尝试在DoWork事件中创建TreeViewItems。正如我先前所写的,据我所知这是不可能的。尝试创建一个不是可视对象(更确切地说是DependencyObjects)的结构作为TreeViewItems的来源,然后在RunWorkerComplete中使用它来构建你的项或作为VM。 - HCL
谢谢HCL,我听从了你的建议,并将FolderBrowser类更改为使用自定义对象来创建结构。 - Albert

1

在任何线程中都不能与 UI 进行交互,但是可以使用 UI Dispatcher 对象在 UI 线程内执行回调函数:

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

获取调度程序更“清晰”的方法是在创建线程时,将它从 UI 对象传递到生成线程的线程/类中。

编辑:

我建议使用 HCL 的解决方案。然而,你在评论中询问如何在避免复制此大型难看代码块的情况下使其工作:

在你的构造函数中,获取一个 Dispatcher 对象的引用,并在你的类中存储它。

然后创建一个像这样的方法:

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

然后像这样调用:

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

你可以用以下方式包装大块代码:

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

如果您试图将太多的代码推回到UI中,那么它与在UI线程中执行所有代码没有什么不同,并且仍然会挂起UI。

然而,HCL的建议是填充自定义树结构,而不是让该代码了解任何UI控件,这样做要好得多 :)


据我所知,他必须在UI线程中创建每个TreeViewItem。因此,这个解决方案可能有点困难(但是可行的)。然而,我不确定我的想法是否正确。 - HCL
我在 GetFolders 方法中使用了你的代码 Application.Current.Dispatcher.Invoke(new System.Action(() => { tviDir = new TreeViewItem() { Header = dir.Name }; })); ,它起作用了,但是在其他修改 tviDir 的地方,我是否应该为每行/代码块使用相同的 Application.Current.Dispatcher.Invoke(...),是否有任何语法可以包含整个方法? - Albert
哦,好的,我想知道是否有一种直接使用UI控件来实现它的方法,但看起来我最好不要在这方面使用它们。谢谢。 - Albert

0

这里提供了一个使用MVVM(至少是视图/视图模型部分)和后台工作线程的答案。它使用后台工作线程递归地填充视图模型,并使用分层数据模板将视图绑定到视图模型。

请注意,我们仍然存在相同的线程问题,因为工作线程无法更改ObservableCollection。因此,我们使用RunWorkerCompleted事件处理程序(在UI线程中执行)来填充集合。

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;
    }
}

0
我建议您研究一下分层模板,而不是手动构建树形结构。您可以在后台线程中构建整个结构,然后将生成的数据结构绑定到您的树形结构。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接