C# 在树视图中加载一堆项目时WPF UI线程冻结

C# 在树视图中加载一堆项目时WPF UI线程冻结,c#,.net,wpf,C#,.net,Wpf,在过去的几个月里,我在TreeView上玩了很多,现在我开始讨论UI冻结问题。当您有大量的项,并且这些项的数据部分创建得非常快,但创建树视图项并将其可视化(必须在UI线程上完成)需要一段时间时,就会出现这种情况 让我们以Shell浏览器和C:\Windows\System32目录为例。(我为此修改了解决方案。)此目录有约2500个文件和文件夹 DataItem和可视化加载是在不同的线程中实现的,但由于文件和目录信息读取速度很快,因此没有任何好处。应用程序在创建树视图项并使其可见时冻结。 我试过:

在过去的几个月里,我在TreeView上玩了很多,现在我开始讨论UI冻结问题。当您有大量的项,并且这些项的数据部分创建得非常快,但创建树视图项并将其可视化(必须在UI线程上完成)需要一段时间时,就会出现这种情况

让我们以Shell浏览器和C:\Windows\System32目录为例。(我为此修改了解决方案。)此目录有约2500个文件和文件夹

DataItem和可视化加载是在不同的线程中实现的,但由于文件和目录信息读取速度很快,因此没有任何好处。应用程序在创建树视图项并使其可见时冻结。 我试过:

  • 在加载项目时,为UI线程设置不同的DispatcherPriority,例如窗口与DispatcherPriority.ContextIdle交互(我可以移动它),但随后项目的加载速度非常慢
  • 创建并可视化块中的项目,比如一次100个项目,但没有任何好处,UI线程仍然处于冻结状态
  • 我的目标是,应用程序将是交互式的,而加载这些项目的! 目前,我只有一个办法来解决这个问题,那就是实现我自己的控制,跟踪窗口大小、滚动条位置并只加载可查看的项目,但这并不容易,我不确定最终性能是否会更好:)

    也许有人知道如何使应用程序在加载一堆可视化项目时具有交互性

    代码:

    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));
        }
    }
    
    <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));
        }
    }
    
    可以在那里找到完整的解决方案:

    节目:

    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));
        }
    }
    
    <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));
        }
    }
    
    公共部分类演示窗口
    {
    公共窗口()
    {
    初始化组件();
    this.Loaded+=DemoWindow\u Loaded;
    }
    私有只读对象_dummyNode=null;
    委托void LoaderDelegate(TreeViewItem tviLoad、字符串strPath、DEL_GetItems actGetItems、AddSubItemDelegate actAddSubItem);
    委托void AddSubItemDelegate(TreeViewItem tviParent,IEnumerable itemsToAdd);
    //获取要加载的项的IEnumerable,在此示例中为“GetFolders”或“GetDrive”
    //运行于:后台线程
    委托IEnumerable DEL_GetItems(字符串strParent);
    已加载void DemoWindow_(对象发送器,路由目标)
    {
    var tviRoot=new TreeViewItem();
    tviRoot.Header=“我的电脑”;
    添加(_dummyNode);
    tviRoot.Expanded+=OnRootExpanded;
    tviRoot.Collapsed+=OnItemCollapsed;
    SetItemImageName(tviRoot,@“Images/Computer.png”);
    foldersTree.Items.Add(tviRoot);
    }
    void OnRootExpanded(对象发送方,RoutedEventArgs e)
    {
    var treeViewItem=e.原始来源为treeViewItem;
    开始加载(treeViewItem、GetDrive、AddItem);
    }
    无效OnItemCollapsed(对象发送方,路由目标)
    {
    var treeViewItem=e.原始来源为treeViewItem;
    if(treeViewItem!=null)
    {
    treeViewItem.Items.Clear();
    treevieItem.Items.Add(_dummyNode);
    }
    }
    void OnFolderExpanded(对象发送方,路由目标)
    {
    var tviSender=e.原始源作为树项;
    e、 已处理=正确;
    开始加载(tviSender、GetFilesAndFolders、AddItem);
    }
    无效开始加载(TreeViewItem tviSender、DEL_GetItems actGetItems、AddSubItems Delegate actAddSubItem)
    {
    tviSender.Items.Clear();
    LoaderDelegate actLoad=加载子项;
    actLoad.BeginInvoke(tviSender,tviSender.tagas字符串,actGetItems,actAddSubItem,ProcessAsyncCallback,actLoad);
    }
    void LoadSubItems(树视图项tviParent、字符串strPath、DEL_GetItems actGetItems、addsubitems委托actAddSubItem)
    {
    var itemsList=actGetItems(strPath).ToList();
    Dispatcher.BeginInvoke(DispatcherPriority.Normal、actAddSubItem、tviParent、itemsList);
    }
    //在后台线程上运行。
    IEnumerable GetFilesAndFolders(字符串strParent)
    {
    var list=Directory.GetDirectories(strParent).Select(itemName=>newitemtoadd(){Path=itemName,typeofitem=ItemType.Directory}).ToList();
    AddRange(Directory.GetFiles(strParent).Select(itemName=>newitemtoadd(){Path=itemName,typeofitem=ItemType.File});
    退货清单;
    }
    //在后台线程上运行。
    IEnumerable GetDrive(字符串strParent)
    {
    返回(Directory.GetLogicalDrives().Select(x=>newitemtoadd(){Path=x,typeofItem=ItemType.DiscDrive}));
    }
    void AddItem(树视图项tviParent,IEnumerable项stoadd)
    {
    字符串imgPath=“”;
    foreach(itemtoadditemtoadd in itemsToAdd)
    {
    开关(项目至项目的添加类型)
    {
    案例ItemType.File:
    imgPath=@“Images/File.png”;
    打破
    案例项目类型。目录:
    imgPath=@“Images/Folder.png”;
    打破
    case ItemType.DiscDrive:
    imgPath=@“Images/DiskDrive.png”;
    打破
    }
    if(itemToAdd.typeofitem==ItemType.Directory | | itemToAdd.typeofitem==ItemType.File)
    IntAddItem(tviParent、System.IO.Path.GetFileName(itemToAdd.Path)、itemToAdd.Path、imgPath);
    其他的
    IntAddItem(tviParent、itemToAdd.Path、itemToAdd.Path、imgPath);
    }            
    }
    私有void IntAddItem(树视图项tviParent、字符串strName、字符串strTag、字符串strImageName)
    {
    var tviSubItem=new TreeViewItem();
    tviSubItem.Header=strName;
    tviSubItem.Tag=strTag;
    添加(_dummyNode);
    tviSubItem.Expanded+=OnFolderExpanded;
    tviSubItem.Collapsed+=OnItemCollapsed;
    TreeViewItemProps.SetItemImageName(tviSub