Reputation: 153
I have an strange issue with a WPF TreeView in C#. When running my App i want to expand and collapse all nodes from the selected TreeView node. When i select a root node, the IsSelected property is not updated. When i manually expand the root node and click on the child node, the IsSelected property is updated. The problem is that when i use the context menu to expand and collapse the child nodes on the root node, the root node is not expanded. When i expand the root node after using the context menu i can see that all child nodes have been expanded correctly.
I have searched a lot in the internet, but couldn't find a similar issue. Furthermore i read a lot of tutorials and howtos on C# WPF TreeViews. So far my implementation seems to me to be correct, thats why i don't understand why the root property IsSelected is not set when clicking on the node.
Can anyone help me? I have implemented the TreeView, ViewModel and Model as follows. Thanks in advance
BR
Michael
<TreeView
Grid.Row="1"
Grid.Column="0"
Margin="5"
ContextMenuOpening="FrameworkElement_OnContextMenuOpening"
ItemsSource="{Binding HierarchialTestObjects}"
SelectedItemChanged="TreeView_OnSelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type vm:HierarchialTestObjectViewModel}" ItemsSource="{Binding Childs}">
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding ExpandAllTreeItemsCommand}" Header="Expand All" />
<MenuItem Command="{Binding CollapseAllTreeItemsCommand}" Header="Collapse All" />
</ContextMenu>
</TreeView.ContextMenu>
namespace mynamespace.subspace.report.data
{
// ... usings removed
public class HierarchialTestObject
{
public string Name { get; set; }
public List<HierarchialTestObject> Childs { get; set; } = new List<HierarchialTestObject>();
public List<string> Path { get; set; } = new List<string>();
public List<string> ParentPath => Path?.AsEnumerable()?.Reverse()?.Skip(1)?.Reverse()?.ToList();
public IHierarchialItem Item { get; set; }
public HierarchialTestObject(IHierarchialItem item)
{
Item = item;
Name = item.ShortDescription;
}
public void SetChilds(IEnumerable<IHierarchialItem> childs)
{
childs.ToList().ForEach(c => Childs?.Add(new HierarchialTestObject(c)));
}
}
}
ViewModel
namespace mynamespace.subspace.report.viewmodel
{
// ... usings removed
public class HierarchialTestObjectViewModel : ViewModelBase
{
public delegate void NotifyTreeItemSelected(HierarchialTestObjectViewModel item);
public event NotifyTreeItemSelected OnTreeItemSelected;
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
SetProperty(ref _isSelected, value, nameof(IsSelected));
if (value)
{
// this is a hack-ish workaround because i cannot use PRISM or other similar libs
OnTreeItemSelected?.Invoke(this);
}
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (_isExpanded != value)
{
SetProperty(ref _isExpanded, value, nameof(IsExpanded));
RaisePropertyChanged(nameof(IconType));
}
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
SetProperty(ref _name, value, nameof(Name));
}
}
}
private IHierarchialItem _item;
public IHierarchialItem Item
{
get { return _item; }
set
{
if (_item != value)
{
SetProperty(ref _item, value, nameof(Item));
}
}
}
private List<string> _path;
public List<string> Path
{
get { return _path; }
set
{
if (_path != value)
{
SetProperty(ref _path, value, nameof(Path));
}
}
}
private ObservableCollection<HierarchialTestObjectViewModel> _childs;
public ObservableCollection<HierarchialTestObjectViewModel> Childs
{
get { return _childs; }
set
{
if (_childs != value)
{
SetProperty(ref _childs, value, nameof(Childs));
}
}
}
public HierarchialTestObjectViewModel(HierarchialTestObject obj)
{
var childs = obj.Childs.Select(c => new HierarchialTestObjectViewModel(c)).ToList();
Path = obj.Path ?? new List<string>();
Childs = new ObservableCollection<HierarchialTestObjectViewModel>(childs);
Item = obj.Item;
Name = obj.Name;
}
public void ExpandRecursively()
{
IsExpanded = true;
Childs?.ToList().ForEach(c => c.ExpandRecursively());
}
public void CollapseRecursively()
{
IsExpanded = false;
Childs?.ToList().ForEach(c => c.CollapseRecursively());
}
}
}
Upvotes: 0
Views: 862
Reputation: 29028
The problem is that you never set the ItemContainerStyle
of the root items. Only for the child items. Therefore the IsExpanded
and IsSelected
properties of the root data models are not connected to their TreeViewItem
data container.
Just set the TreeView.ItemContainerStyle
to solve the issue:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
Remarks
TreeViewItem.IsSelected
is configured with BindingMode.TwoWay
by default.
TreeViewItem.IsSelected
and TreeViewItem.IsExpanded
have their Binding.UpdateSourceTrigger
set to UpdateSourceTrigger.PropertyChanged
by default, which is the default for all Dependency
Property
except they are explicitly set to a different trigger, which is the case for only a few properties (e.g. TextBox.Text
is set to UpdateSourceTrigger.LostFocus
by default).
This way you can reduce code and improve readability.
Upvotes: 2