WPF的UI线程在加载TreeView中大量项目时会冻结

3
在过去的几个月中,我一直在使用TreeView进行实验,现在我遇到了UI冻结问题。当您有大量项目并且这些项目的数据部分非常快速地创建时,但是创建TreeViewItems和可视化这些项目(必须在UI线程上完成)需要时间时,就会出现此问题。
以Shell浏览器和C:\ Windows \ System32目录为例。 (我对http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer解决方案进行了改进。)该目录有约2500个文件和文件夹。
数据项和可视化加载在不同的线程中实现,但由于文件和目录信息很快被读取,因此没有任何好处。当它创建TreeViewItems并使其可见时,应用程序会冻结。
我尝试过: 1.为UI线程设置不同的DispatcherPriorities以加载项目,例如窗口具有交互性(我能够移动它),使用DispatcherPriority.ContextIdle时,但然后项目加载得非常慢。 2.分块创建和可视化项目,例如每次100个项目,但没有任何好处,UI线程仍然会冻结。
我的目标是让应用程序在加载这些项目时变得交互!目前我只有一个想法来解决这个问题,就是实现自己的控件来跟踪窗口大小、滚动条位置,并仅加载可见的项目,但这并不容易做到,而且我不确定最终的性能会更好.. :)
也许有人有办法在加载大量可视化项目时使应用程序变得交互?!
代码:
完整的解决方案可以在这里找到:http://www.speedyshare.com/hksN6/ShellBrowser.zip 程序:
public partial class DemoWindow
{
    public DemoWindow()
    {
        InitializeComponent();
        this.Loaded += DemoWindow_Loaded;
    }

    private readonly object _dummyNode = null;

    delegate void LoaderDelegate(TreeViewItem tviLoad, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem);       
    delegate void AddSubItemDelegate(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd);

    // Gets an IEnumerable for the items to load, in this sample it's either "GetFolders" or "GetDrives"
    // RUNS ON:  Background Thread
    delegate IEnumerable<ItemToAdd> DEL_GetItems(string strParent);

    void DemoWindow_Loaded(object sender, RoutedEventArgs e)
    {
        var tviRoot = new TreeViewItem();

        tviRoot.Header = "My Computer";
        tviRoot.Items.Add(_dummyNode);
        tviRoot.Expanded += OnRootExpanded;
        tviRoot.Collapsed += OnItemCollapsed;
        TreeViewItemProps.SetItemImageName(tviRoot, @"Images/Computer.png");

        foldersTree.Items.Add(tviRoot);
    }

    void OnRootExpanded(object sender, RoutedEventArgs e)
    {
        var treeViewItem = e.OriginalSource as TreeViewItem;

        StartItemLoading(treeViewItem, GetDrives, AddItem);

    }

    void OnItemCollapsed(object sender, RoutedEventArgs e)
    {
        var treeViewItem = e.OriginalSource as TreeViewItem;

        if (treeViewItem != null)
        {
            treeViewItem.Items.Clear();
            treeViewItem.Items.Add(_dummyNode);
        }

    }

    void OnFolderExpanded(object sender, RoutedEventArgs e)
    {
        var tviSender = e.OriginalSource as TreeViewItem;

        e.Handled = true;
        StartItemLoading(tviSender, GetFilesAndFolders, AddItem);
    }

    void StartItemLoading(TreeViewItem tviSender, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
    {
        tviSender.Items.Clear();

        LoaderDelegate actLoad = LoadSubItems;

        actLoad.BeginInvoke(tviSender, tviSender.Tag as string, actGetItems, actAddSubItem, ProcessAsyncCallback, actLoad);
    }

    void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
    {
            var itemsList = actGetItems(strPath).ToList();

            Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList);
    }



    // Runs on Background thread.
    IEnumerable<ItemToAdd> GetFilesAndFolders(string strParent)
    {
        var list = Directory.GetDirectories(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.Directory}).ToList();

        list.AddRange(Directory.GetFiles(strParent).Select(itemName => new ItemToAdd() {Path = itemName, TypeOfTheItem = ItemType.File}));

        return list;
    }

    // Runs on Background thread.
    IEnumerable<ItemToAdd> GetDrives(string strParent)
    {
        return (Directory.GetLogicalDrives().Select(x => new ItemToAdd(){Path = x, TypeOfTheItem = ItemType.DiscDrive}));
    }

    void AddItem(TreeViewItem tviParent, IEnumerable<ItemToAdd> itemsToAdd)
    {
        string imgPath = "";

        foreach (ItemToAdd itemToAdd in itemsToAdd)
        {
            switch (itemToAdd.TypeOfTheItem)
            {
                case ItemType.File:
                    imgPath = @"Images/File.png";
                    break;
                case ItemType.Directory:
                    imgPath = @"Images/Folder.png";
                    break;
                case ItemType.DiscDrive:
                    imgPath = @"Images/DiskDrive.png";
                    break;
            }

            if (itemToAdd.TypeOfTheItem == ItemType.Directory || itemToAdd.TypeOfTheItem == ItemType.File)
                IntAddItem(tviParent, System.IO.Path.GetFileName(itemToAdd.Path), itemToAdd.Path, imgPath);
            else
                IntAddItem(tviParent, itemToAdd.Path, itemToAdd.Path, imgPath);                 
        }            
    }

    private void IntAddItem(TreeViewItem tviParent, string strName, string strTag, string strImageName)
    {
        var tviSubItem = new TreeViewItem();
        tviSubItem.Header = strName;
        tviSubItem.Tag = strTag;
        tviSubItem.Items.Add(_dummyNode);
        tviSubItem.Expanded += OnFolderExpanded;
        tviSubItem.Collapsed += OnItemCollapsed;

        TreeViewItemProps.SetItemImageName(tviSubItem, strImageName);

        tviParent.Items.Add(tviSubItem);
    }

    private void ProcessAsyncCallback(IAsyncResult iAR)
    {
        // Call end invoke on UI thread to process any exceptions, etc.
        Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() => ProcessEndInvoke(iAR)));
    }

    private void ProcessEndInvoke(IAsyncResult iAR)
    {
        try
        {
            var actInvoked = (LoaderDelegate)iAR.AsyncState;
            actInvoked.EndInvoke(iAR);
        }
        catch (Exception ex)
        {
            // Probably should check for useful inner exceptions
            MessageBox.Show(string.Format("Error in ProcessEndInvoke\r\nException:  {0}", ex.Message));
        }
    }

    private struct ItemToAdd
    {
        public string Path;
        public ItemType TypeOfTheItem;
    }

    private enum ItemType
    {
        File,
        Directory,
        DiscDrive
    }
}

public static class TreeViewItemProps
{
    public static string GetItemImageName(DependencyObject obj)
    {
        return (string)obj.GetValue(ItemImageNameProperty);
    }

    public static void SetItemImageName(DependencyObject obj, string value)
    {
        obj.SetValue(ItemImageNameProperty, value);
    }

    public static readonly DependencyProperty ItemImageNameProperty;

    static TreeViewItemProps()
    {
        ItemImageNameProperty = DependencyProperty.RegisterAttached("ItemImageName", typeof(string), typeof(TreeViewItemProps), new UIPropertyMetadata(string.Empty));
    }
}

Xaml:

<Window x:Class="ThreadedWpfExplorer.DemoWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ThreadedWpfExplorer"
    Title="Threaded WPF Explorer" Height="840" Width="350" Icon="/ThreadedWpfExplorer;component/Images/Computer.png">
    <Grid>
        <TreeView x:Name="foldersTree">
            <TreeView.Resources>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="HeaderTemplate">
                        <Setter.Value>
                            <DataTemplate DataType="ContentPresenter">
                                <Grid>
                                    <StackPanel Name="spImg" Orientation="Horizontal">
                                        <Image Name="img"  
                                               Source="{Binding 
                                                           RelativeSource={RelativeSource 
                                                                            Mode=FindAncestor, 
                                                                            AncestorType={x:Type TreeViewItem}},
                                                                            Path=(local:TreeViewItemProps.ItemImageName)}" 
                                               Width="20" Height="20"  Stretch="Fill" VerticalAlignment="Center" />
                                        <TextBlock Text="{Binding}" Margin="5,0" VerticalAlignment="Center" />
                                    </StackPanel>
                                </Grid>

                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </TreeView.Resources>
        </TreeView>
    </Grid>
</Window>

以块状形式替代加载项:

private const int rangeToAdd = 100;

void LoadSubItems(TreeViewItem tviParent, string strPath, DEL_GetItems actGetItems, AddSubItemDelegate actAddSubItem)
{
    var itemsList = actGetItems(strPath).ToList();


    int index;
    for (index = 0; (index + rangeToAdd) <= itemsList.Count && rangeToAdd <= itemsList.Count; index = index + rangeToAdd)
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange(index, rangeToAdd));
    }

    if (itemsList.Count < (index + rangeToAdd) || rangeToAdd > itemsList.Count)
    {
        var itemsLeftToAdd = itemsList.Count % rangeToAdd;

        Dispatcher.BeginInvoke(DispatcherPriority.Normal, actAddSubItem, tviParent, itemsList.GetRange((rangeToAdd > itemsList.Count) ? index : index - rangeToAdd, itemsLeftToAdd));
    }
}
3个回答

3
您要寻找的是UI虚拟化,许多不同的WPF控件都支持它。特别是对于TreeView,请参阅此文章以了解如何打开虚拟化:链接
一个主要的注意事项是,在使用此功能时,您需要使用ItemsSource属性并从集合中提供项目,而不是直接从代码添加项目。这是一个很好的想法,但可能需要一些重构才能使其与您现有的代码正常工作。

谢谢,我一直以为VirtualizingStackPanel.IsVirtualizing="True"是TreeView的默认行为。 - TTT
除此之外,有时使用属性VirtualizingStackPanel.VirtualizationMode="Recycling"的ItemControl的行为是出乎意料的,因为项目容器被重用,但容器的属性似乎并不总是重置。在我的情况下,受害者是IsExpanded属性。当快速拖动滚动条时,会触发展开事件。示例:http://www.speedyshare.com/H9BTV/ShellSolution.zip - TTT

0

为什么不直接创建你的可观察集合并从xaml绑定它呢?

查看MvvM设计模式,只需创建一个类,并将xaml指向它,在那里,从初始化开始,创建您的列表,然后告诉树形视图绑定到该列表,显示列表中每个项目的属性。

我知道这方面的信息有点少,但是做MvvM真的很容易,只需浏览stackoverflow,您就会看到示例。

您真的不需要在每个项目上调用begininvoke-这不仅仅是从mvvm的角度来看的-只需绑定到列表即可。

您还可以对对象使用索引“级别”。


0

另一个有用的技术是数据虚拟化。CodeProject 上有一篇很好的文章和示例项目,介绍了 WPF 中的数据虚拟化


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