Reputation: 73
In a past few months I've played a lot with the TreeView and now I get to the UI freeze problem. It comes when you have large amount of the items and the data part for those Items are created very quickly but creating TreeViewItems and visualizing those (it must be done on UI thread) takes a time.
Let's take Shell browser and C:\Windows\System32 directory as an example. (I reworked http://www.codeproject.com/Articles/24237/A-Multi-Threaded-WPF-TreeView-Explorer solution for that.) This directory has ~2500 files and folders.
The DataItem and Visual loading are implemented in different threads but as the file and directory info are read quickly it gives no benefit. Application freezes when it creates TreeViewItems and makes those visible. I've tried:
My goal is that the application would be interactive while loading those item's! At the moment I have only one idea how to solve this, to implement my own control which tracks window size, scrollbar position and loads only the items which are visable, but it's not so easy to do that and I'm not sure that at the end performance would be better.. :)
Maybe somebody has idea how to make application interactive while loading bunch of visual items?!
Code:
Complete Solution could be found there: http://www.speedyshare.com/hksN6/ShellBrowser.zip
Program:
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>
Alternative Loading items in blocks:
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));
}
}
Upvotes: 3
Views: 4286
Reputation: 10090
Another helpful technique is this regard, is Data Virtualization. There is a good article and sample project on CodeProject, that talks about Data Virtualization in WPF.
Upvotes: 0
Reputation: 27495
What you're looking for is known as UI Virtualization and is supported by a number of different WPF controls. Regarding the TreeView in particular, see this article for details on how to turn on virtualization.
One major caveat is that in order to benefit from this feature, you need to use the ItemsSource property and provide items from a collection rather than adding items directly from your code. This is a good idea to do anyway, but it may require some restructuring to get it functional with your existing code.
Upvotes: 3
Reputation: 421
Why not just create your observable collection and bind to it from xaml?
Check out the MvvM design pattern and you just create a class, and point the xaml at it, in there, from the initialisation, create your list, and then tell the treeview to bind to that list, displaying properties of the each item in your list.
I know this is a little scant on info, but to do MvvM is really easy and just look through stackoverflow and you'll see examples.
You really don't need to call begininvoke on every item - and that's just not from an mvvm point of view - just bind to a list.
You can use indexed 'levels' to your objects too.
Upvotes: 0