在 WPF 应用开发中,TreeView 控件常用于展示层次结构数据,如文件系统、组织架构或分类目录等。本文将详细介绍如何使用 MVVM 模式将 TreeView 控件绑定到 ViewModel 数据源。
在 MVVM 模式中,TreeView 绑定需要以下关键组件:
public class TreeNode : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
private ObservableCollection<TreeNode> _children;
public ObservableCollection<TreeNode> Children
{
get => _children ??= new ObservableCollection<TreeNode>();
set { _children = value; OnPropertyChanged(); }
}
// 支持展开/选中绑定
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set { _isExpanded = value; OnPropertyChanged(); }
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set { _isSelected = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<TreeNode> TreeData { get; } = new();
// 树节点点击命令
public ICommand NodeSelectedCommand { get; }
public MainViewModel()
{
// 初始化树数据
TreeData.Add(new TreeNode
{
Name = "根节点1",
Children =
{
new TreeNode { Name = "子节点1-1" },
new TreeNode
{
Name = "子节点1-2",
Children =
{
new TreeNode { Name = "孙节点1-2-1" }
}
}
}
});
TreeData.Add(new TreeNode
{
Name = "根节点2",
Children =
{
new TreeNode { Name = "子节点2-1" }
}
});
// 初始化命令
NodeSelectedCommand = new RelayCommand<TreeNode>(node =>
{
// 处理节点选中逻辑
Debug.WriteLine($"选中的节点: {node.Name}");
});
}
// INotifyPropertyChanged 实现...
}
<Window ...
xmlns:local="clr-namespace:YourNamespace">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:TreeNode}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="2">
<Image Source="/Resources/folder.png" Width="16" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center"/>
StackPanel>
HierarchicalDataTemplate>
Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding TreeData}" Margin="10">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
"IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
"IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
"MouseDoubleClick" Handler="TreeViewItem_MouseDoubleClick"/>
Style>
TreeView.ItemContainerStyle>
TreeView>
Grid>
Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
private void TreeViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is TreeViewItem item && item.DataContext is TreeNode node)
{
var vm = (MainViewModel)DataContext;
vm.NodeSelectedCommand.Execute(node);
}
}
}
HierarchicalDataTemplate
是树形绑定的核心组件:
ItemsSource
绑定到子节点集合,自动创建树形结构DataType
属性为不同类型节点自动选择模板通过 ItemContainerStyle
实现节点状态的双向绑定:
<Style TargetType="TreeViewItem">
"IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
"IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
Style>
三种命令绑定方式:
直接绑定(需要相对源):
<HierarchicalDataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.NodeCommand,
RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
HierarchicalDataTemplate>
事件处理(后台代码转发):
private void TreeViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// 将事件转发给 ViewModel
}
行为绑定(推荐使用 Microsoft.Xaml.Behaviors):
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Name}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding DataContext.NodeCommand,
RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
i:EventTrigger>
i:Interaction.Triggers>
TextBlock>
HierarchicalDataTemplate>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:FolderNode}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/folder.png" Width="16"/>
<TextBlock Text="{Binding FolderName}" Margin="5,0"/>
StackPanel>
HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:FileNode}">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/file.png" Width="16"/>
<TextBlock Text="{Binding FileName}" Margin="5,0"/>
<TextBlock Text="{Binding Size}" Foreground="Gray"/>
StackPanel>
DataTemplate>
TreeView.Resources>
处理大型树时启用 UI 虚拟化:
<TreeView VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
TreeView>
public class TreeNode : INotifyPropertyChanged
{
// ...
private bool _hasLoaded;
public void LoadChildren()
{
if (_hasLoaded) return;
IsLoading = true;
// 异步加载数据
Task.Run(() =>
{
var data = _service.GetChildren(this.Id);
Application.Current.Dispatcher.Invoke(() =>
{
Children.Clear();
foreach (var item in data)
{
Children.Add(item);
}
IsLoading = false;
_hasLoaded = true;
});
});
}
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set { _isLoading = value; OnPropertyChanged(); }
}
}
<TreeView.Resources>
<HierarchicalDataTemplate ...>
<TextBlock Text="{Binding Name}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="编辑"
Command="{Binding DataContext.EditCommand,
RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
<MenuItem Header="删除"
Command="{Binding DataContext.DeleteCommand,
RelativeSource={RelativeSource AncestorType=TreeView}}"/>
ContextMenu>
TextBlock.ContextMenu>
TextBlock>
HierarchicalDataTemplate>
TreeView.Resources>
解决方案:
ObservableCollection
INotifyPropertyChanged
Application.Current.Dispatcher.Invoke(() =>
{
Children.Add(newNode);
});
检查点:
// 确保命令执行时不会抛出异常
public ICommand NodeCommand => new RelayCommand<object>(param =>
{
try
{
// 命令逻辑
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
});
调试建议:
HierarchicalDataTemplate
的 ItemsSource
绑定路径null
(在 getter 中初始化)public class DebugConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Debugger.Break(); // 调试时在此中断
return value;
}
}
通过以上实现方法和最佳实践,您可以创建出响应式、可维护的树形界面,充分发挥 WPF 数据绑定的强大功能。