2

在过去的几个月里,我玩了很多 TreeView,现在我遇到了 UI 冻结问题。当您拥有大量项目并且这些项目的数据部分创建速度非常快但创建 TreeViewItems 并可视化这些项目(必须在 UI 线程上完成)需要时间时,它就会出现。

我们以 Shell 浏览器和 C:\Windows\System32 目录为例。(我为此重新设计了http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer解决方案。)这个目录有大约 2500 个文件和文件夹。

DataItem 和 Visual 加载在不同的线程中实现,但由于文件和目录信息被快速读取,它没有任何好处。应用程序在创建 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));
    }
}

xml:

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

3 回答 3

3

您正在寻找的内容称为 UI 虚拟化,并受到许多不同 WPF 控件的支持。特别是关于 TreeView,有关如何打开虚拟化的详细信息,请参阅本文。

一个主要的警告是,为了从此功能中受益,您需要使用 ItemsSource 属性并提供集合中的项目,而不是直接从代码中添加项目。无论如何,这是一个好主意,但可能需要进行一些重组才能使其与现有代码一起使用。

于 2013-02-20T21:08:06.033 回答
0

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

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

我知道这在信息上有点少,但是做 MvvM 真的很容易,只要看看 stackoverflow,你就会看到例子。

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

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

于 2013-02-20T20:56:37.323 回答
0

另一个有用的技术是这方面,是数据虚拟化。CodeProject 上有一篇很好的文章和示例项目,它讨论了WPF 中的数据虚拟化。

于 2013-06-14T07:46:22.640 回答