I'm using ICollectionView to display, filter and sort items of a TreeView in WPF. Each tree item has its own collection of sub-items created in the following way:
children = new ObservableCollection<MyTreeItem>();
visibleChildren = CollectionViewSource.GetDefaultView(children);
The filtering consists of two steps. The first one is to go through the whole hierarchy (~600 items total) and chooses, which items are to be displayed and which ones should be filtered out.
private static bool RecursiveApply(StructureTreeBaseItem root,
StructureTreeFilterInfo info) {
bool show = false;
foreach (var child in root.Children)
show &= RecursiveApply(child, info);
if (!show)
show = root.MatchesFilter(info);
root.IsFilteredOut = !show;
return show;
}
The second step is to recursively call Refresh on all ICollectionViews to allow them react to changes.
root.RefreshChildren();
// ->
public void RefreshChildren() {
if (ChildrenChanged)
VisibleChildren.Refresh();
foreach (var child in VisibleChildren)
(child as StructureTreeBaseItem).RefreshChildren();
}
I'm a little bit concerned about performance of such scenario. I made some tests in similar conditions and it turned out, that building the tree structure from scratch takes around 100-150ms, applying filter usually costs no more than 50ms, but recursive calls to Refresh takes 600ms and more.
From what I found, it seems, that it makes a lot more sense to rebuild the tree structure from scratch each time the filter is applied instead of using the ICollectionView features to do it. This seems wrong. Am I doing something not the way I should or maybe it's a general problem with ICollectionView and I should solve this problem in another way?
Edit: (in response to comments)
This is a relevant part of structure of my tree in XAML - nothing special :) The rest is purely presentational.
<TreeView x:Name="tsdTree" DockPanel.Dock="Left" Width="250" Margin="0, 2, 0, 0" ItemsSource="{Binding Path=StructureView}" BorderThickness="0">
<TreeView.Resources>
<HierarchicalDataTemplate ItemsSource="{Binding VisibleChildren}" DataType="{x:Type st:StructureTreeItem}">
(...)
</HierarchicalDataTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding VisibleChildren}" DataType="{x:Type st:StructureTreeFolder}">
(...)
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Related
I'm working on a small "fun" project using WPF/MVVM/Prism which main component is a TreeView showing the file structure of a certain path. The idea how the ViewModel works is taken from Josh Smiths' article http://www.codeproject.com/Articles/28306/Working-with-Checkboxes-in-the-WPF-TreeView.
I actually need two things:
I'd like to get a list of checked items of the TreeView-View, presented in a another view (let's say List-View) and also show that the their state has changed.
I'd like to modify the List-View, which shall then be reflected within the TreeView.
Somehow I did not find a nice solution, as the hierarchical ViewModel used by Josh makes it hard for me to get hold on a "shared" model useable within both ViewModels.
But let's have a look at my code before stating my question:
My "ExplorerView" uses a hierarchical datatemplate and looks as follows:
<UserControl x:Class="MyFunProject.Views.ExplorerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:MyFunProject.ViewModels"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<UserControl.Resources>
<HierarchicalDataTemplate DataType="{x:Type ViewModel:ItemBaseViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="True" IsChecked="{Binding IsChecked}" VerticalAlignment="Center" ToolTip="{Binding Path}">
<TextBlock Text="{Binding Name}" />
</CheckBox>
</StackPanel>
</HierarchicalDataTemplate>
</UserControl.Resources>
<TreeView ItemsSource="{Binding Children}" />
</UserControl>
Following Josh's article, the ExplorerViewModel exhibits a child as a List<CheckableItemViewModel> with only one entry - which in fact has other childs directories or files. Directories itself again have childs.
public class ExplorerViewModel : BindableBase
{
private List<CheckableItemViewModel> childred;
public List<CheckableItemViewModel> Children
{
get { return childred; }
set { SetProperty(ref childred, value); }
}
public ExplorerViewModel(IExplorerModel ExplorerModel)
{
CheckableItemViewModel root = new CheckableItemViewModel();
AddChildrenToNode(root, ExplorerModel.GetCheckableItems());
root.Initialize(); // initialize children (and each children its own children, etc...)
Children = new List<CheckableItemViewModel> { root });
}
private void AddChildrenToNode(CheckableItemViewModel root, IList<CheckableItem> items)
{
foreach(var item in items)
{
var child = new CheckableItemViewModel(item);
var dirItem = item as DirectoryItem; // it's a directory and so it has childs
if(dirItem != null)
{
AddChildrenToNode(child, dirItem.Items);
}
root.Children.Add(child);
}
}
}
And now to my questions:
How do I connect my CheckableItemViewModel to a "kind of" global CheckableItemModel? If I inject the constructor by resolving a registered instance (unity container) I've got the problem, that I cannot do that if I'd like to have two ExplorerViews simultanously (or don't I?).
How do I inject if every CheckableItemViewModel also needs constructor parameters (is this the case where to use parameter override?)
How do I get a retrieve a list of the actual selected items (or when and where should I update the according model)?
How do I get a "changed" flag if one of the CheckableItemViewModel is altered?
Let me know if i missed a piece of my puzzle.
Thanks for any suggestions.
Im getting this error while trying to giva my treeview an itemsource
"Items collection must be empty before using ItemsSource."
I have checked a lot of solutions and I cant seem to find a way to solve this. Here are my code snippets:
XAML:
<HierarchicalDataTemplate x:Key="Category">
<TextBlock Text="{Binding Name}">
</TextBlock>
</HierarchicalDataTemplate>
XAML
<telerik:RadTreeView x:Name="treeview" IsDragDropEnabled="True" HorizontalAlignment="Left" Height="250" Margin="10,10,0,-3" VerticalAlignment="Top" Width="190" IsManipulationEnabled="True" IsLoadOnDemandEnabled="True" LoadOnDemand="treeview_LoadOnDemand" IsExpandOnSingleClickEnabled="True" ItemTemplate="{StaticResource Category}">
</telerik:RadTreeView>
C# - Giving the treeview a data source:
Data d = new Data();
treeview.ItemsSource = d.Get_Categories();
C# - My database query:
public List<Category> Get_Categories()
{
using (var context = new ProcessDatabaseEntities())
{
return context.Category.ToList();
}
}
Category only has two properties, Name and ID. I know that the itemsource-list is not empty when I assign it. So it's probably something wrong with my XAML-code. Thank you in advance!
I believe that your problem is a common one. Basically, you cannot use both the TreeView.ItemsSource and the TreeView.Items properties together... you must choose one way or the other. Usually this problem manifests itself because a developer has done something like this...:
<TreeView Name="TreeView" ItemsSource="{Binding SomeCollection}" ... />
... and then tried to do something like this in the code behind:
TreeView.Items.Add(someItem);
The solution in that case would be to manipulate the data bound collection instead of the TreeView.Items collection:
SomeCollection.Add(someItem);
However, in your case (and it's a little bit difficult to guess without seeing your code), you have probably done the second part first (set or manipulated the Items property) and then tried to set the ItemsSource property. Your solution is the same... use one method of editing the items or the other... not both.
I want to create a dynamic tree view from a list in C# WPF. So I will get all the queues (queue1, ...) and topics (topic1, ...) from a list. Furthermore I need a specific context menu for the different hierarchical points. I want to create a tree view like this:
queues
queue1
queue2
topics
topic1
topic2
There should be a specific context menu for the main point queues and a specific for the main point topics. Additional I need a specific for the subitems queues1 and the topic1. I tried a few things but without success. Has anybody a small example which shows who to solve this problem?
Best Regards
Creating a tree is not an issue. It involves bit of work, but straight forward. You have to create a hierarchical data template from your list and get the tree populated. Following link has all the info.
Creating a wpf tree
OR if you don't want to use sdk
page resource:
tree in xaml:
<Grid TextElement.FontSize="10" DataContext="{StaticResource MyHierarchicalViewSource}" >
<GroupBox x:Name="gbTree">
<TreeView Name="HierarchyTreeview" HorizontalAlignment="Left" AllowDrop="True"
BorderThickness="0" VerticalAlignment="Top" Height="Auto"
ItemsSource="{Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Itemchildren, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Margin="2">
<TextBlock x:Name="text" Text="{Binding Item.ItemLabel}" >
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</GroupBox>
</Grid>
code behing:
Me._HierarchyViewSource = CType(Me.Resources("MyHierarchicalViewSource"), System.Windows.Data.CollectionViewSource)
Me._HierarchyViewSource.Source = your hierarchical data collection
assuming your hierarchy class structure:
Item has
ItemChildren collection
However,I have the same issue in creating a specific context menu. I have posted my own question and haven't found a solution yet. I tried to do it with data triggers with no luck. The only way I know is creating a context menu for the whole tree and make it visible or invisible depending on item type.
If I find a workaround, I will post.
I used the following code for add to TreeView dynamically.
CategorieList - Collection of categories which has id, name, bool value as IsSubCategory and Id of parent category.
private void AddToTree()
{
List<Category> topCategory = CategorieList.Where(c => c.IsSubCategory == false).ToList();
foreach(Category c in topCategory)
{
CategoryTree.Items.Add(CreateTreeViewItem(c));
}
}
private TreeViewItem CreateTreeViewItem(Category category)
{
TreeViewItem tItem = new TreeViewItem();
tItem.Header = category.Name;
List<Category> sub = CategorieList.Where(c => category.Id == c.ParentCategory).ToList();
if (null != sub && sub.Count() != 0)
{
foreach (Category c in sub)
{
TreeViewItem item = CreateTreeViewItem(c);
tItem.Items.Add(item);
}
}
return tItem;
}
XAML code is below
<TreeView x:Name="CategoryTree"> </TreeView>
Kind of an odd problem I have here.
I have a pretty basic recursive tree structure:
public class TreeNode
{
public string Name { get; set; }
public IEnumerable<TreeNode> Children { get; set; }
}
and am displaying the data in a TreeView using a HierarchicalDataTemplate, like so:
<TreeView Name="_tree">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" >
<CheckBox/>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
I populate the tree from code behind:
//allTreeNodes is a list of all existing Tree objects
public void PopulateTree(List<TreeNode> allTreeNodes)
{
foreach (var node in allTreeNodes)
{
_tree.Items.Add(node);
}
}
The result is a TreeView with each existing Tree object as a root node with their respective subtrees.
Note that each TreeNode can be displayed in the tree at multiple locations depending on whether or not it has ancestors.
Up to this point, everything works well, but what I want to do now is only have the checkboxes displayed for the visual root nodes only. I've attempted using a DataTemplateSelector with two HierarchicalDataTemplates, selecting the Template based on an additional boolean property of the TreeNode, but this doesn't work because the TreeNode needs to be displayed with the checkbox once and potentially multiple times without the checkbox.
Any help is appreciated.
Edit: Here is some dummy data to help explain what I want. (Also note that there are no cyclic references in the data.)
+TreeNodeA
-TreeNodeB
-TreeNodeC
-TreeNodeB
+TreeNodeB
+TreeNodeC
-TreeNodeB
+TreeNodeD
-TreeNodeE
-TreeNodeC
-TreeNodeB
+TreeNodeE
-TreeNodeC
-TreeNodeB
In the above view, only the nodes preceded with + should have checkboxes.
Well, I don't know of any straightforward approaches, but what you could do is to create an Attached Property and apply it to each TreeViewItem element. See more about implementing Attached Properties in the Overview and this example on StackOverflow.
This Attached Property would apply a different style to the TreeViewItem depending on the level.
It would be a bit tricky to get the level unless you're programmatically producing the TreeViewItem hierarchy.
If not, you could either calculate it every time a registration of the Attached Property call is made by traversing up the Visual Tree.
Or you could try - should work in theory - by doing RelativeSource binding to an Attached Property of the parent and increment it by 1 using an IValueConverter:
<TreeViewItem TreeViewLevelStyle.Level="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}},
Converter=IncrementByOneValueConverter}">
(Disclaimer - this is an idea, and not something I tried previously)
I have two listBoxes one on the left and one on the right. When I select a 'contactList' item on the left listBox the 'label' information should be displayed on the right listBox and this part works fine. The problem I am having is to do with multi-select because at the moment it will only display the information from one selection. I changed Selection mode in my XAML to multi-select but that did not seem to work. Would appreciate any assistance. Thanks.
XAML
<Grid x:Name="LayoutRoot" Background="#FFCBD5E6">
<ListBox x:Name="contactsList" SelectionMode="Multiple" Margin="7,8,0,7" ItemsSource="{Binding ContactLists, Mode=Default}" ItemTemplate="{DynamicResource ContactsTemplate}" HorizontalAlignment="Left" Width="254" SelectionChanged="contactsList_SelectionChanged"/>
<ListBox x:Name="tagsList" Margin="293,8,8,8" ItemsSource="{Binding AggLabels, Mode=Default}" ItemTemplate="{StaticResource TagsTemplate}" Style="{StaticResource tagsStyle}" />
</Grid>
Code
private void contactsList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (contactsList.SelectedItems.Count > 0)
{
CollectionViewGroup collectionView = contactsList.SelectedItems[0] as CollectionViewGroup;
ContactList selectedContact = contactsList.SelectedItems[0] as ContactList;
ObservableCollection<AggregatedLabel> labelList = new ObservableCollection<AggregatedLabel>();
foreach (ContactList contactList in collectionView.Items)
{
foreach (AggregatedLabel aggLabel in contactList.AggLabels)
{
labelList.Add(aggLabel);
tagsList.ItemsSource = labelList;
}
}
}
}
I think everyone is confused about this part
CollectionViewGroup collectionView = contactsList.SelectedItems[0] as CollectionViewGroup;
ContactList selectedContact = contactsList.SelectedItems[0] as ContactList;
you're only looking at the first selected item. (SelectedItems[0]), but treating it as one thing or another?
you probably need something like
// only create the list once, outside all the loops
ObservableCollection<AggregatedLabel> labelList = new ObservableCollection<AggregatedLabel>();
foreach (var selected in contactsList.SelectedItems)
{
// pretty much your existing code here, referencing selected instead of SelectedItems[0]
}
// only set the list once, outside all the loops
tagsList.ItemsSource = labelList;
ideally, you wouldn't be setting the items source on the tagsList, you'd have that bound to a collection already, and you'd just be replacing the contents in this method. (just one call to clear the collection at the top, and no call to set ItemsSource, since it would have already been bound)
I don't really get what you are doing there at all with all that code but how you normally approach the kind of scenario you described is by binding the second ListBox directly to the first one, should look something like this:
<ListBox Name="ListBox1" ItemsSouce="{Binding SomeOriginalSource}" .../>
<ListBox ItemsSouce="{Binding ElementName=ListBox1, Path=SelectedItems}".../>
Edit: You then can either use a DataTemplate which enumerates the internal collections (which for example could cause you to have a ListBox containing other ListBoxes), or you add a converter to the the binding which merges the internal collections into a single collection like John Gardner noted.