You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在WinUI TreeView中实现多层嵌套的ViewModel展示

WinUI TreeView 多层节点展示解决方案

你的问题核心在于:TreeView 默认要求每个节点的子项是可枚举集合(如 ObservableCollection<T>),但你的 Person、Address 类都是通过单个属性关联子对象(比如 Person.Address 是单个 Address 实例),且未正确配置 HierarchicalDataTemplate 来匹配不同数据类型的节点结构。下面提供两种可行方案:

方案一:修改 ViewModel(推荐,符合 MVVM 设计)

给每个类添加子项集合,自动将关联对象加入集合,让 TreeView 能识别层级关系。同时补全 INotifyPropertyChanged 的实现(你之前的代码仅声明了事件,未触发属性变更):

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Collections.Generic;

public class Person : INotifyPropertyChanged
{
    private string _name;
    private Address _address;
    public ObservableCollection<object> Children { get; } = new();

    public string Name
    {
        get => _name;
        set => SetField(ref _name, value);
    }

    public Address Address
    {
        get => _address;
        set
        {
            if (SetField(ref _address, value))
            {
                Children.Clear();
                if (value != null)
                    Children.Add(value);
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

public class Address : INotifyPropertyChanged
{
    private string _street;
    private Postcode _postcode;
    public ObservableCollection<object> Children { get; } = new();

    public string Street
    {
        get => _street;
        set => SetField(ref _street, value);
    }

    public Postcode Postcode
    {
        get => _postcode;
        set
        {
            if (SetField(ref _postcode, value))
            {
                Children.Clear();
                if (value != null)
                    Children.Add(value);
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

public class Postcode : INotifyPropertyChanged
{
    private string _code;
    private string _name;

    public string Code
    {
        get => _code;
        set => SetField(ref _code, value);
    }

    public string Name
    {
        get => _name;
        set => SetField(ref _name, value);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

对应的 XAML 配置

给每个数据类型定义 HierarchicalDataTemplate,指定子项来源和显示模板:

<TreeView x:Name="PersonTreeView">
    <TreeView.Resources>
        <!-- 邮编节点模板(无子项) -->
        <DataTemplate x:Key="PostcodeTemplate" DataType="local:Postcode">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="邮编:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Code}"/>
                <TextBlock Text=" (" Margin="4,0"/>
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text=")"/>
            </StackPanel>
        </DataTemplate>

        <!-- 地址节点模板(子项为邮编) -->
        <HierarchicalDataTemplate x:Key="AddressTemplate" 
                                  DataType="local:Address"
                                  ItemsSource="{Binding Children}"
                                  ItemTemplate="{StaticResource PostcodeTemplate}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="地址:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Street}"/>
            </StackPanel>
        </HierarchicalDataTemplate>

        <!-- 人员节点模板(子项为地址) -->
        <HierarchicalDataTemplate x:Key="PersonTemplate" 
                                  DataType="local:Person"
                                  ItemsSource="{Binding Children}"
                                  ItemTemplate="{StaticResource AddressTemplate}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="姓名:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Name}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
    <TreeView.ItemTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding}" ContentTemplate="{StaticResource PersonTemplate}"/>
        </DataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

后台绑定数据源

public MainWindow()
{
    this.InitializeComponent();

    // 构造测试数据
    var postcode = new Postcode { Code = "100000", Name = "北京市" };
    var address = new Address { Street = "某某街道123号", Postcode = postcode };
    var person = new Person { Name = "张三", Address = address };

    PersonTreeView.ItemsSource = new ObservableCollection<Person> { person };
}

方案二:不修改 ViewModel,用转换器将单个对象转集合

如果不想改动现有 ViewModel,可通过转换器把单个对象包装成集合,让 TreeView 识别为子项:

转换器代码

using Microsoft.UI.Xaml.Data;
using System.Collections.Generic;

public class SingleItemToCollectionConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return value == null ? new List<object>() : new List<object> { value };
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

对应的 XAML 配置

直接绑定单个属性,通过转换器转为集合:

<TreeView x:Name="PersonTreeView">
    <TreeView.Resources>
        <local:SingleItemToCollectionConverter x:Key="SingleToCollection"/>

        <DataTemplate x:Key="PostcodeTemplate" DataType="local:Postcode">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="邮编:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Code}"/>
                <TextBlock Text=" (" Margin="4,0"/>
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text=")"/>
            </StackPanel>
        </DataTemplate>

        <HierarchicalDataTemplate x:Key="AddressTemplate" 
                                  DataType="local:Address"
                                  ItemsSource="{Binding Postcode, Converter={StaticResource SingleToCollection}}"
                                  ItemTemplate="{StaticResource PostcodeTemplate}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="地址:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Street}"/>
            </StackPanel>
        </HierarchicalDataTemplate>

        <HierarchicalDataTemplate x:Key="PersonTemplate" 
                                  DataType="local:Person"
                                  ItemsSource="{Binding Address, Converter={StaticResource SingleToCollection}}"
                                  ItemTemplate="{StaticResource AddressTemplate}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="姓名:" FontWeight="SemiBold"/>
                <TextBlock Text="{Binding Name}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
    <TreeView.ItemTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding}" ContentTemplate="{StaticResource PersonTemplate}"/>
        </DataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

注意事项

  • 确保 XAML 中的 local 命名空间指向你的 ViewModel 所在命名空间,例如 xmlns:local="using:YourProjectNamespace"
  • 两种方案都能实现三层节点(Person → Address → Postcode)的展开/折叠展示

内容的提问来源于stack exchange,提问作者Patrik Valyon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 16:15:21