WPF MVVM TreeView使用HierarchicalDataTemplate未更新

3

我一直在努力解决我的TreeView无法正确更新的问题,所以我想问问是否有人能告诉我为什么我的代码不能正确地在添加或删除时更新TreeView节点。提前道歉,因为我要贴出相当大量的代码,但我觉得这些都很重要,以便说明问题。

首先是我的ObservableObject类

public abstract class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

TreeNodeBase 类

public abstract class TreeNodeBase : ObservableObject
{
    protected const string ChildNodesPropertyName = "ChildNodes";

    protected string name;

    public string Name
    {
        get
        {
            return this.name;
        }

        set
        {
            this.name = value;
            this.OnPropertyChanged();
        }
    }

    protected IList<TreeNode> childNodes;

    protected TreeNodeBase(string name)
    {
        this.Name = name;
        this.childNodes = new List<TreeNode>();
    }

    public IEnumerable<TreeNode> ChildNodes
    {
        get
        {
            return this.childNodes;
        }
    }

    public TreeNodeBase AddChildNode(string name)
    {
        var treeNode = new TreeNode(this, name);
        this.childNodes.Add(treeNode);
        this.OnPropertyChanged(ChildNodesPropertyName);

        return treeNode;
    }

    public TreeNode RemoveChildNode(string name)
    {
        var nodeToRemove = this.childNodes.FirstOrDefault(node => node.Name.Equals(name));

        if (nodeToRemove != null)
        {
            this.childNodes.Remove(nodeToRemove);
            this.OnPropertyChanged(ChildNodesPropertyName);
        }

        return nodeToRemove;
    }
}

public class TreeNode : TreeNodeBase
{
    public TreeNodeBase Parent { get; protected set; }

    public TreeNode(TreeNodeBase parent, string name)
        : base(name)
    {
        this.Parent = parent;
    }
}

TreeNodeRoot类

public class TreeViewRoot : TreeNodeBase
{
    public TreeViewRoot(string name)
        : base(name)
    {
    }
}

树节点类

public class TreeNode : TreeNodeBase
{
    public TreeNodeBase Parent { get; protected set; }

    public TreeNode(TreeNodeBase parent, string name)
        : base(name)
    {
        this.Parent = parent;
    }
}

TreeView用户控件Xaml

<UserControl x:Class="TreeViewExperiment.TreeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:treeViewExperiment="clr-namespace:TreeViewExperiment"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="400"
             d:DataContext="{d:DesignInstance treeViewExperiment:TreeViewmodel}">

    <UserControl.DataContext>
        <treeViewExperiment:TreeViewmodel/>
    </UserControl.DataContext>

    <Grid Background="White">
        <Grid.Resources>
            <HierarchicalDataTemplate x:Key="TreeViewHierarchicalTemplate" ItemsSource="{Binding ChildNodes}">
                <TextBlock Text="{Binding Name}"/>
            </HierarchicalDataTemplate>

            <Style TargetType="Button">
                <Setter Property="FontFamily" Value="Verdana"/>
                <Setter Property="FontWeight" Value="Bold"/>
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="6*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TreeView Grid.Row="0" x:Name="Tree" ItemsSource="{Binding RootLevelNodes}" ItemTemplate="{StaticResource TreeViewHierarchicalTemplate}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectedItemChanged">
                    <i:InvokeCommandAction
                        Command="{Binding SetSelectedNode}"
                        CommandParameter="{Binding SelectedItem, ElementName=Tree}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TreeView>

        <Grid Grid.Row="1" Height="25">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="4*"/>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>

            <TextBox x:Name="NameTextBox" Grid.Column="0" VerticalAlignment="Center" FontFamily="Verdana"/>
            <Button Grid.Column="1" Content="Add Node" Command="{Binding AddNode}" CommandParameter="{Binding Text, ElementName=NameTextBox}" Background="Green"/>
            <Button Grid.Column="2" Content="Remove Node" Command="{Binding RemoveNode}" Background="Red"/>
        </Grid>
    </Grid>
</UserControl>

最后是TreeView模型

public class TreeViewmodel : ObservableObject
{
    public ICommand SetSelectedNode { get; private set; }

    public ICommand AddNode { get; private set; }

    public ICommand RemoveNode { get; private set; }

    public TreeViewmodel()
    {
        this.SetSelectedNode = new ParamaterizedDelegateCommand(
            node =>
                {
                    this.SelectedTreeNode = (TreeNodeBase)node;
                });

        this.AddNode = new ParamaterizedDelegateCommand(name => this.SelectedTreeNode.AddChildNode((string)name));

        this.RemoveNode = new DelegateCommand(
            () =>
                {
                    if (selectedTreeNode.GetType() == typeof(TreeNode))
                    {
                        var parent = ((TreeNode)this.SelectedTreeNode).Parent;
                        parent.RemoveChildNode(this.SelectedTreeNode.Name);
                        this.SelectedTreeNode = parent;
                    }
                });

        var adam = new TreeViewRoot("Adam");
        var steve = adam.AddChildNode("Steve");
        steve.AddChildNode("Jack");

        this.rootLevelNodes = new List<TreeViewRoot> { adam, new TreeViewRoot("Eve") };
    }

    private TreeNodeBase selectedTreeNode;

    private readonly IList<TreeViewRoot> rootLevelNodes;

    public IEnumerable<TreeViewRoot> RootLevelNodes
    {
        get
        {
            return this.rootLevelNodes;
        }
    }

    public TreeNodeBase SelectedTreeNode
    {
        get
        {
            return this.selectedTreeNode;
        }

        set
        {
            this.selectedTreeNode = value;
            this.OnPropertyChanged();
        }
    }
}

我知道当子元素被添加或删除时,UI应该得到通知,因为在调试时,我可以看到ChildNodes属性的get访问器在这两种情况下都被调用,然而在UI上显示的内容仍然没有改变。

过去,我通过使用ObservableCollections解决了这个问题,在StackOverflow上大多数关于这种问题的解决方案似乎都指向这一点,但是为什么这个解决方案也不起作用呢?我错过了什么吗?

1个回答

3
问题在于您误用了INotifyPropertyChanged。在您的代码中,您通知视图已更改您的ChildNodes属性,但实际情况并非如此,因为TreeViewItem.ItemsSource仍等于您的ChildNodes属性。 INotifyPropertyChanged将在视图模型中的基础集合对象更改时导致UI更新。
要在集合中出现新项目时更新ItemsSource,您需要使用实现INotifyCollectionChanged的集合。
正如MSDN所说:
“您可以枚举任何实现IEnumerable接口的集合。但是,要设置动态绑定,以便插入或删除集合中的项自动更新UI,集合必须实现INotifyCollectionChanged接口。该接口公开了一个事件,应在基础集合更改时引发。”
这就是为什么每个人都建议使用ObservableCollection的原因。
编辑:
如果要公开只读集合,则应检查ReadOnlyObservableCollection<T>。它作为ObservableCollection的包装器工作,可以使其非公共。

这似乎是问题所在。但对我来说仍然很重要,因为我想向UI公开一个只读的集合,所以我想摆脱ObservableCollection。我可能会将现有列表拆分成自己的类,并实现INotifyCollectionChanged接口,看看效果如何。 - Thermonuclear
也许你应该考虑使用ReadOnlyObservableCollection<T> Class类? - Patryk Spytkowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接