c# wpf TreeView 选择根项的问题

c# wpf TreeView issue with selecting root item

我对 C# 中的 WPF TreeView 有一个奇怪的问题。当 运行 我的应用程序时,我想展开和折叠 selected TreeView 节点中的所有节点。当iselect一个根节点时,IsSelected属性不更新。当我手动展开根节点并单击子节点时,IsSelected 属性 会更新。问题是当我使用上下文菜单展开和折叠根节点上的子节点时,根节点没有展开。当我使用上下文菜单展开根节点时,我可以看到所有子节点都已正确展开。

我在网上搜索了很多,都没有找到类似的问题。此外,我阅读了很多关于 C# WPF TreeViews 的教程和指南。到目前为止,我的实现在我看来是正确的,这就是为什么我不明白为什么在单击节点时未设置 root 属性 IsSelected 的原因。

谁能帮帮我?我已经按如下方式实现了 TreeView、ViewModel 和 Model。提前致谢

BR

迈克尔


<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)));
    }
}
}

视图模型

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());
    }

}
}

问题是您从未设置根项目的 ItemContainerStyle。仅适用于子项。因此,根数据模型的 IsExpandedIsSelected 属性未连接到它们的 TreeViewItem 数据容器。

只需设置TreeView.ItemContainerStyle即可解决问题:

<TreeView.ItemContainerStyle>
  <Style TargetType="{x:Type TreeViewItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected}" />
    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
  </Style>  
</TreeView.ItemContainerStyle>

备注

TreeViewItem.IsSelected默认配置为BindingMode.TwoWay
TreeViewItem.IsSelectedTreeViewItem.IsExpandedBinding.UpdateSourceTrigger 默认设置为 UpdateSourceTrigger.PropertyChanged,这是 all Dependency Property 的默认设置,除了它们被显式设置为不同的触发器,只有少数属性是这种情况(例如 TextBox.Text 默认设置为 UpdateSourceTrigger.LostFocus)。
这样您可以减少代码并提高可读性。