如何在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
相关产品推荐
相关产品推荐

