在过去的几个月中,我一直在使用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 程序:
以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));
}
}