How to access the child elements of a TreeViewItem - c#

I am trying to add a search feature to a TreeView in WPF. I was successfully able to add and search the tree items for a TreeView which has just one level, but for a TreeView with multiple levels, I am not able to figure out how to access the child controls (UIElement) of a TreeViewItem. ItemContainerGenerator.ContainerFromItem() returns null when I pass TreeViewItem.Item as an argument. I saw few posts which discussed the same issue, but none of the solutions worked for me.
Here's my XAML code.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="72*" />
</Grid.RowDefinitions>
<StackPanel x:Name="SearchTextBox" Grid.Row="0" Margin="5,5,5,0"/>
<TreeView Name="MyTreeView" Grid.Row="1" BorderThickness="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Students}">
<TextBlock Margin="3" Text="{Binding SchoolName}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding StudentName}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
Here's my C# code
public class MyClass
{
public string ClassName { get; set; }
public List<Student> Students { get; set; }
}
public class Student
{
public string StudentName { get; set; }
}
private void PlanSearchOnSearchInputChanged(string searchQuery)
{
List<TreeViewItem> items = GetLogicalChildCollection(MyTreeView);
}
public static List<TreeViewItem> GetLogicalChildCollection(TreeView parent)
{
var logicalCollection = new List<TreeViewItem>();
foreach (var child in parent.Items)
{
TreeViewItem item = (TreeViewItem)parent.ItemContainerGenerator.ContainerFromItem(child);
logicalCollection.Add(item);
GetLogicalChildCollection(item, logicalCollection);
}
return logicalCollection;
}
public static List<TreeViewItem> GetLogicalChildCollection(TreeViewItem parent, List<TreeViewItem> logicalCollection)
{
foreach (var child in parent.Items)
{
TreeViewItem item = (TreeViewItem)parent.ItemContainerGenerator.ContainerFromItem(child);
logicalCollection.Add(item);
}
return logicalCollection;
}
PS: I need access to UIElement of the item, not just item.
Any help will be greatly appreciated.

If the TreeViewItem is not expanded then its children will not have any UI so you can't get their children. Maybe that's the problem you're seeing.
This is some code to think about, I don't have all your code to easily work out what's happening but this works on a sample I have. It is quick and dirty rather than super elegant.
private void Button_Click(object sender, RoutedEventArgs e)
{
foreach (var item in tv.Items)
{
TreeViewItem tvi = tv.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
tvi.IsExpanded = true;
tvi.UpdateLayout();
RecurseItem(tvi);
}
}
private bool gotTheItem = false;
private void RecurseItem(TreeViewItem item)
{
foreach (var subItem in item.Items)
{
TreeViewItem tvi = item.ItemContainerGenerator.ContainerFromItem(subItem) as TreeViewItem;
// do something
if (!gotTheItem)
{
RecurseItem(tvi);
}
}
}
You could set IsExpanded back or hook the event fires when you expand an item and probably some other things.

The TV has a nodes collection. Each node also has a nodes collection.
You need to start at the root, examine each node, and if that node contains a nodes collection with a length > 0, you need to descend into it.
This is typically done with a recursive tree walk.

Related

How to fill a wpf tree view using mvvm from a model with "infinte" levels

I have that entity framework model that self inner where a user can make sub categories to sub categories for as many as necessary.
public class Category
{
public Category()
{
SubCategories = new ObservableCollection<Category>();
}
[Column("id")]
public int Id { get; set; }
[StringLength(100)]
public string Name { get; set; }
[Column("ParentID")]
public int? ParentID { get; set; }
[ForeignKey("ParentID")]
public virtual ObservableCollection<Category> SubCategories { get; set; }
}
I was thinking of populating a tree view with it using foreach like this:
Categories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == null));
foreach (var item in Categories)
{
SubCategoriesModel = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == item.Id));
foreach (var subitem in SubCategoriesModel)
{
item.SubCategories.Add(subitem);
}
}
<TreeView Grid.Row="0" ItemsSource="{Binding Categories}" MinWidth="220">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type data:Categories}" ItemsSource="{Binding SubCategories}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Id}" Margin="3 2" />
<TextBlock Text=" - "/>
<TextBlock Text="{Binding Name}" Margin="3 2" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
<e:Interaction.Behaviors>
<behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedTreeCategory, Mode=TwoWay}" />
</e:Interaction.Behaviors>
</TreeView>
I realized that this not going to work. Is there a better way to do it?
Method 1
When you are dealing with a potentially infinite number of sub-levels (for example because items can reference each other and would cause an infinite loop during recursion), I'd recommend populating the items when they are first expanded.
Method 2
If you don't have recursions and want to load all data at once, you can simply do so by loading it in a recursive method (be careful though - if the levels go too deep you might get a StackOverflowException)
Example Method 1
A very simple viewmodel for this situation could look like this:
public class Node
{
public uint NodeId { get; set; }
public string DisplayName { get; set; }
public bool ChildrenLoaded { get; set; }
public ObservableCollection<Node> Children { get; set; }
public Node()
{
ChildrenLoaded = false;
Children = new ObservableCollection<Node>();
}
public void LoadChildNodes()
{
if (ChildrenLoaded) return;
// e.g. Every SubCategory with a parentId of this NodeId
var newChildren = whereverYourDataComesFrom.LoadChildNodes(NodeId);
Children.Clear();
foreach (Node child in newChildren)
Children.Add(child);
ChildrenLoaded = true;
}
}
Set the Treeview up like this, where Nodes is one or more root nodes that you load first. (Categories with ParentId = null in your example)
<TreeView TreeViewItem.Expanded="TreeViewItem_Expanded" ItemsSource="{Binding Nodes}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding DisplayName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The TreeViewItem.Expanded event is a so called RoutedEvent btw. It's not fired by the TreeView, but the TreeViewItems themselves and just bubbles up your visual tree (that's the actual technical term for it, there is also tunneling and direct).
Whenever a node is expanded for the first time you simply load all the child nodes in the TreeViewItem_Expanded event handler.
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
Node node = ((FrameworkElement)e.OriginalSource).DataContext as Node;
if (node == null) return;
node.LoadChildNodes();
}
So no matter how many items you have and if the reference each other, you only load the root nodes and everything else happens on-demand.
Translating that principle to your specific example, I'd simply split the load method of your data into your root Category entries and load the SubCategories in the Expanded event handler instead of pre-loading everything.
Since most of your code is already almost identical, I think this should be a rather easy modification.
Example Method 2
private void LoadRootCategories()
{
Categories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == null));
foreach (var item in Categories)
{
LoadSubCategories(item)
}
}
private void LoadSubCategories(Category item)
{
item.SubCategories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == item.Id));
foreach (var subitem in item.SubCategories)
{
// Recursive call
LoadSubCategories(subitem);
}
}

Not able to make bindings in treeview

Im trying to make a custom treeview with an itemtemplate, so I can show the headertext + a type of the item in the treeview.
My inspiration comes from this answer;
https://stackoverflow.com/a/33119107/9156219
So, my problem is that I cant make the Itembindings work.
Here's my code;
XAML:
<TreeView x:Name="treeView" ItemsSource="{Binding treeList}" Grid.Column="0" IsVisibleChanged="treeView_IsVisibleChanged" SelectedItemChanged="treeView_SelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Type}" Margin="2,2,2,2" Background="LightBlue" FontSize='8'/>
<TextBlock Text="{Binding SystemName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
C#
public class CustomTreeViewItem : TreeViewItem
{
public String SystemName { get; set; }
public String Type { get; set; }
public String ParentItem { get; set; }
public String Path { get; set; }
}
public List<CustomTreeViewItem> treeList = new List<CustomTreeViewItem>();
public void SetRootNode()
{
int itmNumber = datSet.Rows.Count;
for (int i = 0; i < itmNumber; i++)
{
treeList.Add(new CustomTreeViewItem
{
SystemName = (string)datSet.Rows[i].ItemArray[1].ToString(),
Type = (string)datSet.Rows[i].ItemArray[2].ToString(),
ParentItem = (string)datSet.Rows[i].ItemArray[3].ToString(),
Path = (string)datSet.Rows[i].ItemArray[4].ToString(),
});
treeList[i].Header = treeList[i].SystemName;
}
foreach (CustomTreeViewItem item in treeList.Where(treeList => treeList.ParentItem == ""))
{
treeView.Items.Add(item);
}
foreach (CustomTreeViewItem item in treeList.Where(treeList => treeList.ParentItem != "").Where(treeList => treeList.Type != "Signal"))
{
var test = treeList.Find(treeList => treeList.SystemName == item.ParentItem);
test.Items.Add(item);
}
}
SetRootNode() is being called in the beginning of the program. datSet is being filled with a OleDBDataAdapter.
In the treeview, only the SystemName is being showed and not the type. What am I doing wrong?
Thank you in advance!
You are trying to create a TreeView whose TreeViewItems contain yet again TreeViewItems, if you remove the inheritance for your model, it works:
do this:
public class CustomTreeViewItem
instead of
public class CustomTreeViewItem : TreeViewItem
is this good enough for your needs?

Why doesn't the following WPF treeview work?

It's been a really long time since iv'e used WPF, now, required to do a small project on it, I have a really weird problem making a simple databinded treeview.
the main window so far:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
populateTreeview();
}
private XMLDataNode tree;
public XMLDataNode TreeRoot
{
get { return tree; }
set
{
tree = value;
NotifyPropertyChanged("TreeRoot");
}
}
//Open the XML file, and start to populate the treeview
private void populateTreeview()
{
{
try
{
//Just a good practice -- change the cursor to a
//wait cursor while the nodes populate
this.Cursor = Cursors.Wait;
string filePath = System.IO.Path.GetFullPath("TestXML.xml");
TreeRoot = RXML.IOManager.GetXMLFromFile(filePath).Last();
}
catch (XmlException xExc)
//Exception is thrown is there is an error in the Xml
{
MessageBox.Show(xExc.Message);
}
catch (Exception ex) //General exception
{
MessageBox.Show(ex.Message);
}
finally
{
this.Cursor = Cursors.AppStarting; //Change the cursor back
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
The data class i'm working with which loads XML nodes into itself:
public class XMLDataNode
{
public XmlNode node;
public string Title;
public List<XMLDataNode> Children;
public XMLDataNode(XmlNode XMLNode)
{
this.node = XMLNode;
this.Title = node.ToString();
Children = new List<XMLDataNode>();
foreach (XmlNode item in XMLNode.ChildNodes)
{
Children.Add(new XMLDataNode(item));
}
}
the mainwindow XAML:
<Window x:Class="RotemXMLEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="800" Width="1000" x:Name="me">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TreeView Grid.Row="1" x:Name="treeView" Background="White" ItemsSource="{Binding ElementName=me, Path=TreeRoot}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Title}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
The current output is an empty treeview, with nothing in it, even though the databinded object is filled with info.
I realize it's probably a silly mistake, but I really can't seem to find the mistake, Help?
Can you turn this property in a collection that implements IEnumerable?
public XMLDataNode TreeRoot
e.g.:
public XMLDataNode[] TreeRoots
The ItemsSource of the TreeView expects a type that implements IEnumerable
This works for me:
<TreeView Grid.Row="1" x:Name="treeView" Background="White" ItemsSource="{Binding ElementName=me, Path=TreeRoots}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Title}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
private XMLDataNode[] treeRoots;
public XMLDataNode[] TreeRoots
{
get { return treeRoots; }
set
{
treeRoots = value;
NotifyPropertyChanged("TreeRoots");
}
}
private void populateTreeview()
{
{
try
{
//Just a good practice -- change the cursor to a
//wait cursor while the nodes populate
this.Cursor = Cursors.Wait;
string filePath = System.IO.Path.GetFullPath("TestXML.xml");
TreeRoot = RXML.IOManager.GetXMLFromFile(filePath).Last();
TreeRoots = new[] { TreeRoot };
}
catch (XmlException xExc)
I have found the solution.
WPF databinding can only work (which I forgot) with properties, not fields, even though the fields are public, changing all the data binded fields to actual properties solves this problem.

get parent-note in treeview when the child-items are the same object

i have a TreeView with same in xaml createt TreeViewItems. And one note has a ObservableCollection as ItemSource. This works like a Charm. But now i want same Notes to every item of the list (for better organization). So i do this:
This is my HierarchicalDataTemplate for the liste
<HierarchicalDataTemplate DataType="{x:Type classes:Connection}" ItemsSource="{Binding Source={StaticResource ConnectionChilds}}" >
<TextBlock Text="{Binding DisplayName}" />
</HierarchicalDataTemplate>
And the ItemsSource:
<collections:ArrayList x:Key="ConnectionChilds">
<classes:TreeItemObject ItemsSourcePath="Child1" />
<classes:TreeItemObject ItemsSourcePath="Child2" />
<classes:TreeItemObject ItemsSourcePath="Child3" />
</collections:ArrayList>
TreeItemObject is a simple Class:
public class TreeItemObject
{
public string ItemsSourcePath { get; set; }
}
And last but not least HierarchicalDataTemplate for TreeItemObject:
<DataTemplate DataType="{x:Type classes:TreeItemObject}">
<TextBlock Margin="5,0" Text="{Binding Path=ItemsSourcePath}"/>
</DataTemplate>
Looked like this
Connection 1
Child1
Child2
Child3
Connection 2
Child1
Child2
Child3
Connection 3
Child1
Child2
Child3
Works perfekt. But now if i select "Connection 2\Child3" i got the same object like "Connection 1\Child3" or "Connection 3\Child3". Ok make sense because based on same object. With that situation i have no chance to find out the parent-note on OnSelectedItemChanged.
Because if i search with this extension-Class. I only get the first expanded Connection-Note.
http://social.msdn.microsoft.com/Forums/silverlight/en-US/84cd3a27-6b17-48e6-8f8a-e5737601fdac/treeviewitemcontainergeneratorcontainerfromitem-returns-null?forum=silverlightnet
Is there a way to find the real parent in the TreeView?
I personally do not like the idea of creating clones within a converter but I do not know the full scope of your problem. So working with what you have presented here, we can achieve the assignment of a parent to each TreeItemObject via a MultiValueConverter.
WPF has an awesome feature called MultiBinding. It processes 1 or more source values into a single target. To do this, it needs a multivalue converter.
So, change the TreeItemObject to
public class TreeItemObject
{
public string ItemsSourcePath { get; set; }
public WeakReference Parent { get; set; }
}
The hierarchicalDataTemplate for the Connection type would become
<HierarchicalDataTemplate DataType="{x:Type classes:Connection}">
<HierarchicalDataTemplate.ItemsSource>
<MultiBinding Converter="{StaticResource items2Clones}">
<Binding Source="{StaticResource ConnectionChilds}" />
<Binding />
</MultiBinding>
</HierarchicalDataTemplate.ItemsSource>
<TextBlock Text="{Binding DisplayName}" />
</HierarchicalDataTemplate>
Based on the above binding, to set the parent in the converter, the Convert method in your convert will be along the lines
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var destinationItems = new Collection<TreeItemObject>();
var items = values[0] as Collection<TreeItemObject>;
var parent = values[1] as Connection;
// null checks are required here for items and parent
foreach (var item in items)
{
var destinationItem = item.Clone(); // Assumed extension method
destinationItem.Parent = new WeakReference(parent);
destinationItems.Add(destinationItem);
}
return destinationItems;
}
Finally, the SelectedItemChanged event handler would be something like
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var item = (TreeItemObject)e.NewValue;
if ((item != null) && (item.Parent != null) && (item.Parent.IsAlive))
{
// do stuff - Console.WriteLine(((Connection)item.Parent.Target).DisplayName);
}
}
I have removed exception management and some null checking for brevity.
I hope this helps
I think your only choice is to clone your children before you add them to the TreeView, allowing at least a binary difference between the child nodes.
If you do this, instead of handling the OnSelectedItemChanged event and traversing the object graph, add a WeakReference of the parent to each of its children. This will allow you to immediately reference the parent from the child and also allow .Net to clean up the object graph correctly.
An example of using a WeakReference property from a SelectedItemChanged event handler is as follows
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = sender as TreeView;
var item = treeView.SelectedItem as TreeItemObject;
if (item.Parent.IsAlive)
{
var parent = item.Parent.Target;
}
}
I have removed exception management and null checking for brevity.
I hope this helps.
It's difficult to get the parent from the treeView item so what i did is that, i had a member property of the parent of the parent type which holds the reference to the parent as below
public class FileSystem :NotifyChange, IEnumerable
{
#region Private members
private ObservableCollection<FileSystem> subDirectoriesField;
#endregion
#region Public properties
/// <summary>
/// Gets and sets all the Files in the current folder
/// </summary>
public ObservableCollection<FileSystem> SubDirectories
{
get
{
return subDirectoriesField;
}
set
{
if (subDirectoriesField != value)
{
subDirectoriesField = value;
RaisePropertyChanged("SubDirectories");
}
}
}
/// <summary>
/// Gets or sets name of the file system
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// Gets or sets full path of the file system
/// </summary>
public string FullPath
{
get;
set;
}
/// <summary>
/// object of parent, null if the current node is root
/// </summary>
public FileSystem Parent
{
get;
set;
}
public FileSystem(string fullPath, FileSystem parent)
{
Name = fullPath != null ? fullPath.Split(new char[] { System.IO.Path.DirectorySeparatorChar },
StringSplitOptions.RemoveEmptyEntries).Last()
FullPath = fullPath;
Parent = parent;
AddSubDirectories(fullPath);
}
public IEnumerator GetEnumerator()
{
return SubDirectories.GetEnumerator();
}
private void AddSubDirectories(string fullPath)
{
string[] subDirectories = Directory.GetDirectories(fullPath);
SubDirectories = new ObservableCollection<FileSystem>();
foreach (string directory in subDirectories)
{
SubDirectories.Add(new FileSystem(directory, this));
}
}
}
And my viewModel is as below
public class ViewModel:NotifyChange
{
private ObservableCollection<FileSystem> directories;
public ObservableCollection<FileSystem> Directories
{
get
{
return directoriesField;
}
set
{
directoriesField = value;
RaisePropertyChanged("Directories");
}
}
public ViewModel()
{
//The below code has to be moved to thread for better user expericen since when UI is loaded it might not respond for some time since it is looping through all the drives and it;s directories
Directories=new ObservableCollection<FileSystem>();
Directories.Add(new FileSystem("C:\\", null);
Directories.Add(new FileSystem("D:\\", null);
Directories.Add(new FileSystem("E:\\", null);
}
}
Since each child knows it;s parent now you can traverse back, root node parent will be null
Xmal will have the following
<TreeView Grid.Row="1" Background="Transparent" ItemsSource="{Binding Directories}" Margin="0,10,0,0" Name="FolderListTreeView"
Height="Auto" HorizontalAlignment="Stretch" Width="300" >
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:FileSystem}" ItemsSource="{Binding SubDirectories}">
<Label Content="{Binding Path= Name}" Name="NodeLabel" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Hope this helps you

How bind an object to treeview from Xaml

I am trying to bind an object to the treeviewcontrol WPF by XAML, I am getting the treview as empty. When i am doing that by treeview.items.add(GetNode()) then its working.
I am using MVVM Framework(caliburn.Micro) I wanted to do it in Xaml, how do I assign Item source property in xaml? I tried with creating a property of Node class and calling the Method GetNode() with in the property, and assigned that property as itemssource of the treeview and changed the List to Observable collection. Still issue is same.
Working Xaml when doing treeview.items.Add(GetNode()) which returns a Node and and i as assigning Nodes collection to Hireachial Template.
<TreeView Name="treeview2"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
ItemContainerStyle="{StaticResource StretchTreeViewItemStyle}" Width="300">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}">
<DockPanel LastChildFill="True">
<TextBlock Padding="15,0,30,0" Text="{Binding Path=numitems}" TextAlignment="Right"
DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Path=Text}" DockPanel.Dock="Left" TextAlignment="Left" />
</DockPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Server Side Code:
this.treeview2.Items.Add(GetNode());
GetNode recursively build a list of type Node.
public class Node
{
public string Text { get; set; }
public List<Node> Nodes { get; set; }
public ObservableCollection<Node> Nodes{get;set;} // with list and observable collection same results
public int numitems { get; set; }
}
In addition to the HierarchicalDataTemplate, which seems just fine, add a binding to the ItemsSource property of your TreeView:
public class ViewModel
{
private List<Node> _rootNodes;
public List<Node> RootNodes
{
get
{
return _rootNodes;
}
set
{
_rootNodes = value;
NotifyPropertyChange(() => RootNodes);
}
}
public ViewModel()
{
RootNodes = new List<Node>{new Node(){Text = "This is a Root Node}",
new Node(){Text = "This is the Second Root Node"}};
}
And in XAML:
<TreeView ItemsSource="{Binding RootNodes}"
.... />
Edit: Remove the call that does this.Treeview.... you don't need that. Try to keep to the minimum the amount of code that references UI Elements. You can do everything with bindings and have no need to manipulate UI Elements in code.

Categories

Resources