Reputation: 93
I am a bit of a novice to C# and recently started using MVVMLight. Here's a basic overview of my current project and problem. My application will read the filesystem/directory structure and recreate it in a TreeView. The user then checks a CheckBox to specify which files to process. If a user checks a directory, all sub items of that directory will be checked recursively.
The directories often contain hundreds of files, so several thousands of file objects are created in a given instance. The check boxes are bound two-way to a property ToBeProcessed on my view model. Here is a snippet of that code (Directory and File objects both derive from TreeViewModelBase).
public class TreeViewModelBase : ObservableObject
{
private bool _toBeProcessed = false;
public bool ToBeProcessed
{
get
{
return _toBeProcessed;
}
set
{
if (_toBeProcessed != value)
{
_toBeProcessed = value;
RaisePropertyChanged(nameof(ToBeProcessed));
}
}
}
...
}
Additionally I have a method on my MainViewModel that checks/unchecks everything. This property is CheckAll and is a boolean bound to a checkbox. When the "Check All" checkbox is checked, a command is initiated which sets all children's ToBeProccessed property to the value of CheckAll.
public class MainViewModel : ViewModelBase
{
...
private async void ToggleAll()
{
// Prevent tree from being modified while the values are being updated.
TreeIsEnabled = false; // Bound to IsEnabled on TreeView
// My attempt to update the UI from another thread.
await Task.Run(() =>
{
// Update each child's ViewModel
foreach (TreeViewModelBase child in GetChildren())
{
child.ToBeProcessed = CheckAll;
}
});
// Re-enable the tree when the await is finished.
TreeIsEnabled = true;
}
...
}
Everything works as expected, but the main problem is while the UI is rendering the thousands of RaisePropertyChanged events, my UI is completely unresponsive. I'm somewhat sure my awaited Task here is not doing anything, because the UI thread still needs to render everything and therein lies my bottle neck. I've searched somewhat extensively on a solution, but the solutions I found either do not work for me, or I can't find a way to implement their solution into my case.
Thank you for your time!
Upvotes: 1
Views: 458
Reputation: 93
I arrived to my solution thanks to Stephen Cleary. https://stackoverflow.com/a/42422827/4748204
I simply added to my TreeView:
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
In my case, I also had a Border wrapping my TreeView. The border seemed to cause WPF to ignore the TreeView's virtualization and caused items out of view to be rendered unless I also added the virtualizing properties above to the border as well. Adding these virtualization properties caused the UI to only render the CheckBox changes for the items on the screen. The rest of the CheckBoxes are rendered on demand as they come into view. The result is great and in the end required very little modification of my existing code. I removed the await from my ToggleAll method. I will also likely remove the TreeIsEnabled, because the performance is now so fast that you can't even perceive the disable/enable.
Upvotes: 0
Reputation: 2285
Please understand that using the await keywords incurs:
Both operations slow down the program's responsiveness, since they are carried out on the UI.
If all you are doing is update the UI, and you do not need to really act on this information, then there's no need to create a Task. This only hurts performance, since creating a task, too allocates memory.
What you could do is "re-design" how you look at the problem. You do not need to logically update all of the children. You can let WPF take care of visually toggling them, not even needing to Invoke INPC, which too incurs the cost of Reflection.
This sounds like a job for a Trigger, Something along the lines of:
<Checkbox x:Name="SelectAll"/>
... Tree Element Check Box:
<CheckBox IsChecked="{Binding IsSelected, Mode=OneWayToSource}">
<CheckBox.Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SelectAll, Path=IsChecked}" Value="True">
<Setter Property="IsChecked" Value="True" />
</DataTrigger>
</Style.Triggers>
</CheckBox.Style>
</CheckBox>
The Trigger simply toggles the Checkboxes checked state, which in turn toggle the Descendant's view model boolean property named IsSelected, which in turn does nothing, simply persisting the selection's state.
What you also get out of the box, is that when the select all checkbox is unchecked, each textbox reverts to its previous state. You can override this if this is not desired, by simply binding between each child and the select all check box.
all you need to do, when the selection information is actually required, is to loop through the selected items and process them.
Upvotes: 0
Reputation: 407
Try this instead.
private void ToggleAll()
{
// Prevent tree from being modified while the values are being updated.
TreeIsEnabled = false; // Bound to IsEnabled on TreeView
// My attempt to update the UI from another thread.
Task task = new Task(() =>
{
// Update each child's ViewModel
foreach (TreeViewModelBase child in GetChildren())
{
child.ToBeProcessed = CheckAll;
}
});
task.Start();
// Re-enable the tree when the await is finished.
TreeIsEnabled = true;
}
Upvotes: 0
Reputation: 456437
Thousands of UI elements are too many. No UI system is designed with that in mind, because a user simply is not capable of interacting with thousands of controls on a single form.
I've searched somewhat extensively on a solution, but the solutions I found either do not work for me, or I can't find a way to implement their solution into my case.
It sounds like you should use a kind of virtualization. The idea is that instead of building the entire tree immediately, you just build the parts you need to display. When the user expands a node, then you add to the tree just the files/subfolders at that folder.
Upvotes: 1
Reputation: 72
Would a Parallel.ForEach loop here work for you here?
https://msdn.microsoft.com/en-us/library/dd460720(v=vs.110).aspx
instead of this:
foreach (TreeViewModelBase child in GetChildren())
{
child.ToBeUploaded = CheckAll;
}
Upvotes: 0