2013-02-20 42 views
2

在過去幾個月中,我玩了很多TreeView,現在我遇到了UI凍結問題。當你有大量的項目和這些項目的數據部分創建非常快,但創建TreeViewItems和可視化這些(它必須在UI線程上完成)需要一段時間。WPF UI線程凍結在TreeView中加載一堆項目

讓我們以Shell瀏覽器和C:\ Windows \ System32目錄爲例。 (我重新編寫了http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer解決方案。)這個目錄有大約2500個文件和文件夾。

DataItem和Visual加載在不同的線程中實現,但由於文件和目錄信息被快速讀取,它沒有任何好處。當它創建TreeViewItems並使其可見時,應用程序會凍結。 我已經試過:

  1. 設置不同的DispatcherPriorities的UI線程時加載的項目,例如窗口被交互(我是能夠移動)與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

您正在查找的內容稱爲UI虛擬化,並且受到許多不同WPF控件的支持。有關TreeView的詳細信息,請參閱this article瞭解如何開啓虛擬化的詳細信息。

一個主要的警告是,爲了從這個功能中受益,你需要使用ItemsSource屬性並提供集合中的項目,而不是直接從代碼中添加項目。無論如何,這是一個好主意,但它可能需要進行一些重構才能使其與現有代碼一起使用。

+0

謝謝,我在想某種程度上VirtualizingStackPanel.IsVirtualizing =「True」是默認的TreeView行爲。 – TTT 2013-02-21 13:39:28

+0

除了有時ItemControl與屬性VirtualizingStackPanel.VirtualizationMode =「回收」行爲是意外的,因爲項目容器被重用,但容器的屬性似乎並不總是重置。在我的情況下,受害者是IsExpanded財產。通過快速拖動滾動條觸發擴展事件。例如:http://www.speedyshare.com/H9BTV/ShellSolution.zip – TTT 2013-02-21 14:39:27

0

爲什麼不只是創建您觀察到的收集和XAML綁定到它?

檢查出MvvM的設計模式,你只需要創建一個類,然後將xaml指向它,從初始化開始,創建你的列表,然後告訴樹視圖綁定到該列表,顯示列表中的每個項目。

我知道這是信息的一點缺乏,但要做MvvM真的很容易,只需通過stackoverflow看,你會看到的例子。

你真的不需要在每個項目上調用begininvoke - 而這僅僅是從mvvm的角度來看 - 只是綁定到一個列表。

您也可以使用索引的「水平」。

0

另一個有用的技術是數據虛擬化。 CodeProject上有一篇很好的文章和示例項目,講述了Data Virtualization in WPF.