Silverlight treeview: save expanded/collapsed state - c#

I'm using a hierarchical tree view in Silverlight 4. This tree can be cleared and rebuilded quite often, depending on the user's actions. When this happends, the tree is collapsed by default, which can be annoying from a user perspective.
So, I want to somehow save which nodes are expanded so I can restore the visual state of my tree after it is clear and reloaded.
My treeview is implemented like this:
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:controls2="clr-namespace:System.Windows;assembly=System.Windows.Controls"
<controls:TreeView x:Name="Tree"
ItemsSource="{Binding Source={StaticResource ViewModel}, Path=TreeStructure, Mode=OneWay}"
ItemTemplate="{StaticResource hierarchicalTemplate}" />
<controls2:HierarchicalDataTemplate x:Key="hierarchicalTemplate" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Value.DisplayName}">
</controls2:HierarchicalDataTemplate>
ItemsSource of my treeview is bound on an ObservableCollection TreeStructure;
Node is a wrapper class that looks like that:
public class Node
{
public object Value { get; private set; }
public ObservableCollection<Node> Children { get; private set; }
public Node(object value)
{
Value = value;
Children = new ObservableCollection<Node>();
}
}
Pretty standard stuff. I saw some solutions for WPF, but I can find anything for the Silverlight tree view...
Any suggestions?
Thanks!

Given the way you are implementing your data as a tree, why not bind the 'TreeViewItem.IsExpanded` dependency property to a bool property on your own Node?
It will need to be an INotifyPropertyChanged property at a minimum so Node will need to implement INotifyPropertyChanged.
In Silverlight 5 you can just set a style like this to bind to the IsExpanded property:
<Style TargetType="sdk:TreeViewItem" x:Key="itemStyle">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
And use with
ItemContainerStyle="{Binding Source={StaticResource itemStyle}}"
In Silverlight 4 there are a number of workarounds.

Here's what I did to bind on the TreeViewItem.IsExpanded property. First, I added an IsExpanded property in my Node class.
public class Node : INotifyPropertyChanged
{
public object Value { get; private set; }
public ObservableCollection<Node> Children { get; private set; }
private bool isExpanded;
public bool IsExpanded
{
get
{
return this.isExpanded;
}
set
{
if (this.isExpanded != value)
{
this.isExpanded = value;
NotifyPropertyChanged("IsExpanded");
}
}
}
public Node(object value)
{
Value = value;
Children = new ObservableCollection<Node>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
After that, I subclassed the TreeView and TreeViewItem controls (I lose the custom theme on my treeview, but whatever...)
public class BindableTreeView : TreeView
{
protected override DependencyObject GetContainerForItemOverride()
{
var itm = new BindableTreeViewItem();
itm.SetBinding(TreeViewItem.IsExpandedProperty, new Binding("IsExpanded") { Mode = BindingMode.TwoWay });
return itm;
}
}
public class BindableTreeViewItem : TreeViewItem
{
protected override DependencyObject GetContainerForItemOverride()
{
var itm = new BindableTreeViewItem();
itm.SetBinding(TreeViewItem.IsExpandedProperty, new Binding("IsExpanded") { Mode = BindingMode.TwoWay });
return itm;
}
}
In my XAML, I just have to use BindableTreeView instead of TreeView, and it works.

The trick is to use SetterValueBindingHelper from here. Then your XAML will look like the following. Make sure you carefully copy what I have below.
<sdk:TreeView.ItemContainerStyle>
<Style TargetType="sdk:TreeViewItem">
<Setter Property="local:SetterValueBindingHelper.PropertyBinding">
<Setter.Value>
<local:SetterValueBindingHelper>
<local:SetterValueBindingHelper Property="IsSelected" Binding="{Binding Mode=TwoWay, Path=IsSelected}"/>
<local:SetterValueBindingHelper Property="IsExpanded" Binding="{Binding Mode=TwoWay, Path=IsExpanded}"/>
</local:SetterValueBindingHelper>
</Setter.Value>
</Setter>
</Style>
</sdk:TreeView.ItemContainerStyle>
The syntax isn't exactly like what you would use in WPF, but it works and it works well!

Related

WPF TreeView, TwoWay binding for IsExpanded is not affecting GUI from C# code

I am trying to create a TreeView that can display items in a tree hirearchy. I want to be able to use code (C#) to expand and collapse TreeViewItems in the TreeView, through the properties bound to an ObservableCollection.
I have bound a property of my class to IsExpanded, and it seems to work if I set it BEFORE setting the tree's ItemSource - the newly created hierarchy will arrive pre-expanded.
But if I click a button that sets IsExpanded for an item in the collection, it does not expand or collapse the tree items in the GUI.
Here is an ugly screenshot of the program so far. The folders were manually created in an Initialize procedure.
Here is the TreeView xaml in the main window:
<TreeView x:Name="TheProjectTree" Margin="5" BorderBrush="{x:Null}" ItemsSource="{Binding DataSet}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
<!--<EventSetter Event="Expanded" Handler="TheProjectTreeItem_Expanded" />-->
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding nodes}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Path=Icon}" Height="16"/>
<TextBlock Text=" " />
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text=" (" />
<TextBlock Text="{Binding Path=Type}" />
<TextBlock Text=")" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Here is a MyProject class that has the data structures:
using System.Collections.ObjectModel;
namespace Project_X
{
public class MyProject
{
public ObservableCollection<MyNode> nodes;
public MyProject()
{
}
public void Initialize()
{
nodes = new ObservableCollection<MyNode>();
nodes.Add(new MyNode("Untitled Project", "Project"));
AddFolder("0. Initialize");
AddFolder("1. Reset");
AddFolder("2. Migrate");
}
public void AddFolder(string folderName)
{
nodes[0].nodes.Add(new MyProject.MyNode(folderName, "Folder"));
}
public class MyNode
{
public string Name { get; set; }
public string Type { get; set; }
public bool IsExpanded { get; set; }
public ObservableCollection<MyNode> nodes { get; set; }
public MyNode(string theName, string theType)
{
Name = theName;
Type = theType;
nodes = new ObservableCollection<MyNode>();
}
public string Icon
{
get
{
if (Type == "Project")
return "./graphics/icon_projectTree_small.png";
else if (Type == "Folder")
return "./graphics/icon_projectTree_small.png";
else if (Type == "Object")
return "./graphics/icon_projectTree_small.png";
else if (Type == "SQL")
return "./graphics/icon_projectTree_small.png";
else if (Type == "Text")
return "./graphics/icon_projectTree_small.png";
return "./graphics/icon_projectTree_small.png";
}
}
}
}
}
And finally, here is a little test procedure that I can call from a testing button.
private void NewProject()
{
Project = new MyProject(); // fire up the main project variable!
Project.Initialize(); // give it some dummy data!
Project.nodes[0].IsExpanded = true; // pre-expand the top-level project node
TheProjectTree.ItemsSource = Project.nodes; // assign the data set to the tree in the main window
Project.AddFolder("test"); // this works! adding new folders to the collection will show up in the GUI
Project.nodes[0].IsExpanded = true; // this does NOT work! it should collapse the top-levl project node in the tree, but it doesn't
}
I would greatly appreciate it if you could brow-beat some knowledge into me. I usually work in SQL, C# and .NET are not my strong suit. I spent the whole evening trying to wrap my head around MVVM and goodness I now feel like a really crummy programmer!
Implement INotifyPropertyChanged interface to your MyNode Class. Which notifies that the property value has changed.
public class MyNode : INotifyPropertyChanged
{
private bool isExpanded;
public string Name { get; set; }
public string Type { get; set; }
public bool IsExpanded
{
get => isExpanded;
set
{
isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
public ObservableCollection<MyNode> nodes { get; set; }
public MyNode(string theName, string theType)
{
Name = theName;
Type = theType;
nodes = new ObservableCollection<MyNode>();
}
public event PropertyChangedEventHandler PropertyChanged;
}
Your MyNode class need to implement INotifyPropertyChanged
to let the Gui know that the property has changed.
Then in the setter of the IsExpanded property you will have to call NotifyPropertyChanged has explained in the given link.

How to notify ViewModel when properties binded to an ObservableCollection change?

I'm using a HierarchicalDataTemplate to build a menubar that will have MenuItems that are checkable. In my ViewModel, I create an ObservableCollection of a class called MenuItemModel, then bind the ObservableCollection in my View. I can build the menu along with its submenus, but I can't figure out how to tell the ViewModel which MenuItem is checked.
I've tried using the INotifyPropertyChanged in the MenuItemModel but I could not figure out how to send that information to the ViewModel. After much googling, I've come to the conclusion this isn't the proper approach and I only need to use the INotifyPropertyChanged in the ViewModel. I'm a WPF newbie so still learning the do's and don'ts. I've found most of the code below on StackOverflow and have managed to adapt it to to my needs but I'm still trying to wrap my head around how it works. The code below will create a menu "Main Menu" with 3 submenus "SubMenu1", "SubMenu2", and "SubMenu3" where they are all checkable. That being said, here are my questions:
How/where do I implement the OnPropertyChanged event in the ViewModel when a MenuItem is checked/unchecked?
How can I access model properties of the MenuItem that was checked/unchecked?
<Menu DockPanel.Dock="Top" ItemsSource="{Binding MenuItemsObservableCollection}">
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="IsCheckable" Value="{Binding IsCheckable}" />
<Setter Property="StaysOpenOnClick" Value="{Binding IsCheckable}" />
<Setter Property="IsChecked" Value="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</Menu.ItemContainerStyle>
<Menu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ViewModel}" ItemsSource="{Binding Path=SubMenuItemsObservableCollection}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
AddMenuItems();
}
private void AddMenuItems()
{
var subMenu = new ObservableCollection<MenuItemModel>
{
new MenuItemModel { Header = "SubMenu1" },
new MenuItemModel { Header = "SubMenu2"},
new MenuItemModel { Header = "SubMenu3"}
};
MenuItemsObservableCollection = new ObservableCollection<MenuItemModel>
{
new MenuItemModel { Header = "Main Menu", SubMenuItemsObservableCollection = subMenu }
};
}
private void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<MenuItemModel> MenuItemsObservableCollection { get; set; }
}
class MenuItemModel
{
public MenuItemModel()
{
}
public string Header { get; set; }
public string Key { get; set; }
public bool IsCheckable { get; set; } = true
public bool IsChecked { get; set; } = false;
public ObservableCollection<MenuItemModel> SubMenuItemsObservableCollection { get; set; }
}

PropertyChanged always null with TreeView

I'm having a problem implementing a search functionality in my TreeView on a WPF Project.
I used this guide to create a TreeView with ViewModels. I have used the TextSeachDemo and edited the Controls in a way that they that they fit my application. (Added the right Classes, more layers etc.)
Everything works fine, I get a structure with correct children and parents and the search function also works, as it finds the correct entry.
Problem now is: When i try to set the "IsExpanded" Property from code nothing happens. Debubgging shows me that the PropertyChanged event in the RaiseProperty Changed Method is always null.
In the test Project provided by Josh Smith, everything seems to be working fine.
The only significant difference that i could make out is that he set the datacontext in code while i did in the XAML:
Code from Josh Smith:
public TextSearchDemoControl()
{
InitializeComponent();
// Get raw family tree data from a database.
Person rootPerson = Database.GetFamilyTree();
// Create UI-friendly wrappers around the
// raw data objects (i.e. the view-model).
_familyTree = new FamilyTreeViewModel(rootPerson);
// Let the UI bind to the view-model.
base.DataContext = _familyTree;
}
From the constructor from the MainViewModel (The ViewModel that handles the entire window)
List<FactoryItem> rootItems = _machineService.GetFactoryItems();
FactoryTree = new FactoryTreeViewModel(rootItems);
Where as FactoryTree is a public Observable Property which i bind the DataContext of the TreeView too (instead of in code as above):
<TreeView DataContext="{Binding FactoryTree}" ItemsSource="{Binding FirstGeneration}">
The other way around by the way, when i open a item via the GUI, the breakpoint on my property does trigger and raise an event.
Any ideas?
This solution addresses the problem in a more mvvm friendly way.
A UserControl contains a TreeView.
It uses the type YourViewModel as data context.
The view model contains a collection of YourDomainType which itself has a child collection ChildElements of the same type.
In xaml, the data is bound to the ElementInViewModel collection of the view model. In addition there is a HierarchicalDataTemplate (which is appropriate for a tree view).
The type YourDomainType contains properties IsExpanded and IsSelected which are bound to the respective properties of the TreeViewItem in a Style.
If you set these properties in your view model the tree view should react as expected (selecting or expanding the respective TreeViewItem).
I am aware that IsExpanded and IsSelected do not belong to a DTO object.
YourDomainType is probably more a type for displaying data, but it could also wrap a DTO object stored in it.
<UserControl>
<UserControl.DataContext>
<YourViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<CollectionViewSource Source="{Binding Path=ElementsInViewModel}" x:Key="Cvs">
</CollectionViewSource>
<HierarchicalDataTemplate DataType="{x:Type DomainModel:YourDomainType}"
ItemsSource="{Binding Path=ChildElements}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Setter>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Setter>
</Style>
</UserControl.Resources>
<DockPanel>
<TreeView ItemsSource="{Binding Source={StaticResource Cvs}}"/>
</DockPanel>
</UserControl>
public class YourViewModel
{
public ObservableCollection<YourDomainType> ElementsInViewModel
{
get
{
return _elementsInViewModel;
}
set
{
if (_elementsInViewModel != value)
{
_elementsInViewModel = value;
RaisePropertyChanged("ElementsInViewModel");
}
}
}
ObservableCollection<YourDomainType> _elementsInViewModel;
public YourViewModel()
{
}
}
public class YourDomainType
{
public ObservableCollection<YourDomainType> ChildElements
{
get
{
return _childElements;
}
set
{
if (_childElements != value)
{
_childElements = value;
RaisePropertyChanged("ChildElements");
}
}
}
ObservableCollection<YourDomainType> _childElements;
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
if (_isExpanded != value)
{
_isExpanded = value;
RaisePropertyChanged("IsExpanded");
}
}
}
bool _isExpanded;
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (_isSelected != value)
{
_isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
bool _isSelected;
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
RaisePropertyChanged("Name");
}
}
}
string _name;
}

Access item in observable collection on item selected

I have a WPF TreeView populated by an observable collection using a hiarchialdatabinding
I need to access the item in my observable collection or the database that was used to populate it.
An example use case is that the user right clicks a treeview item to add a subgroup. I obviously need to access its parent to add the child.
Any suggestions? Im so lost..
I cant just edit the treeview item itself cause then the changes wont reflect back to my database
Database Code:
[Serializable]
public class LoginGroup
{
public string Name { get; set; }
public Guid ID { get; set; }
public List<Login> LoginItems = new List<Login>();
public List<LoginGroup> Children { get; set; }
}
public static ObservableCollection<LoginGroup> _GroupCollection = new ObservableCollection<LoginGroup>();
public ObservableCollection<LoginGroup> GroupCollection
{
get { return _GroupCollection; }
}
TreeView:
<TreeView x:Name="groupView" Width="211" TreeViewItem.Selected="OnTreeItemSelected" DockPanel.Dock="Left" Height="Auto" ItemsSource="{Binding GroupCollection}" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
You can just cast the SelectedItem to LoginGroup:
LoginGroup selectedGroup = (LoginGroup)groupView.SelectedItem;
You can't reflect back changed of your properties because they don't have way to "notice" back that they are edited. You need inherit LoginGroup from DependencyObject or implement INotifyPropertyChanged
You should use TreeView's ItemContainer style.
Here's sample TreeNode view model:
public class TreeNode : ViewModel
{
public TreeNode()
{
this.children = new ObservableCollection<TreeNode>();
// the magic goes here
this.addChildCommand = new RelayCommand(obj => AddNewChild());
}
private void AddNewChild()
{
// create new child instance
var child = new TreeNode
{
Name = "This is a new child node.",
IsSelected = true // new child should be selected
};
// add it to collection
children.Add(child);
// expand this node, we want to look at the new child node
IsExpanded = true;
}
public String Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
OnPropertyChanged("Name");
}
}
}
private String name;
public Boolean IsSelected
{
get { return isSelected; }
set
{
if (isSelected != value)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
private Boolean isSelected;
public Boolean IsExpanded
{
get { return isExpanded; }
set
{
if (isExpanded != value)
{
isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
}
private Boolean isExpanded;
public ObservableCollection<TreeNode> Children
{
get { return children; }
}
private ObservableCollection<TreeNode> children;
public ICommand AddChildCommand
{
get { return addChildCommand; }
}
private RelayCommand addChildCommand;
}
Some comments:
ViewModel is any base implementation of INotifyPropertyChanged
interface.
RelayCommand (a.k.a. DelegateCommand) is ICommand implementation for use in MVVM approach.
Here's the view:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<TreeView ItemsSource="{Binding}">
<TreeView.ItemContainerStyle>
<!-- Let's glue our view models with the view! -->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<!-- Here's menu item, which is responsible for adding new child node -->
<MenuItem Header="Add child..." Command="{Binding AddChildCommand}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Window>
... and sample data context initialization:
public MainWindow()
{
InitializeComponent();
DataContext = new ObservableCollection<TreeNode>
{
new TreeNode { Name = "Root", IsSelected = true }
};
}
Hope this helps.
Upd.
Of course, you have to expose child nodes as the ObservableCollection too. Otherwise, changes made to nodes collection won't be reflected.

IsExpanded only works on first level of TreeView

I'm using a TreeView with HierarchicalDataTemplate but can't get the IsExpanded property working for higher levels than the first. Here's my xaml:
<TreeView>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
In my ResourceDictionary I have:
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
what makes the first order work.
In higher indention levels IsExpanded is always false because the PropertyChangedEventHandler is not fired for children.
Here's my class:
public class ListItem : INotifyPropertyChanged
{
private bool isExpanded;
public bool IsExpanded
{
get { return isExpanded; }
set
{
if (isExpanded != value)
{
isExpanded = value;
SendPropertyChanged("IsExpanded");
}
}
}
private void SendPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<ListItem> Children { get; set; }
...
}
EDIT: I'm very sorry, my corrected code is working!
If you want to automatically expand all the children as well the target item then you need to propogate the change downwards yourself, do something like this....
public bool IsExpanded
{
get { return isExpanded; }
set
{
if (isExpanded != value)
{
isExpanded = value;
if (isExpanded)
{
foreach(ListItem child in Children)
child.IsExpanded = true;
}
SendPropertyChanged("IsExpanded");
}
}
}

Categories

Resources