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

WPF MVVM实现层级TreeView:根据ValueType动态渲染控件

WPF TreeView 根据ValueType动态渲染控件解决方案

问题背景

现有ViewModel结构如下:

// 顶层策略ViewModel
class MainWindowVM {
    // 对应每个应用角色的ViewModel集合
    public List<RolePolicyVM> ComponentPolicyVMs { get; set; } = new();
}

// 对应特定应用角色的组件策略
class RolePolicyVM {
    // 角色名称
    public String Name { get; set; }
    
    // 应用于该角色的组件策略集合
    public List<ComponentPolicyVM> ComponentPolicies { get; set; } = new();
}

// 对应特定代理插件或工作项的组件策略
class ComponentPolicyVM {
    // 插件或工作项ID(不渲染)
    public Guid Id { get; set; }
    
    // 在AgentConfig工具中显示的设置描述
    public String Description { get; set; }
    
    // 鼠标悬停在描述文本块时显示的提示文本
    public String ToolTipText { get; set; }
    
    // 绑定到控件的值
    public Object Value { get; set; }
    
    // "Value"的数据类型,决定渲染的控件类型
    public Type ValueType { get; set; }
}

需要构建基于MainWindowVM.ComponentPolicyVMs的层级TreeView:

  • 顶层层级绑定RolePolicyVM.Name
  • 二级层级包含两列:
    • 第一列绑定ComponentPolicyVM.Description
    • 第二列绑定ComponentPolicyVM.Value,并根据ValueType动态渲染控件:typeof(bool)显示CheckBox,typeof(string)显示TextBox,typeof(int)显示数值输入控件

目前已实现TreeView但仅能渲染CheckBox,现有XAML代码如下:

<Window x:Class="AppPolicyConfig.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:AppPolicyConfig.ViewModels"
        xmlns:local="clr-namespace:AppPolicyConfig"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainWindowVM/>
    </Window.DataContext>
    <TreeView x:Name="MainTreeView"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Stretch"
              Margin="10"
              ItemsSource="{Binding ComponentPolicyVMs}">
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsExpanded" Value="True" />
            </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding ComponentPolicies}" DataType="{x:Type vm:RolePolicyVM}">
                <TextBlock Text="{Binding Name}"/>
                <HierarchicalDataTemplate.ItemTemplate>
                    <DataTemplate DataType="{x:Type vm:ComponentPolicyVM}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="300" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" 
                                       Text="{Binding Description}"/>
                            <CheckBox Grid.Column="1"
                                      IsChecked="{Binding Value}"/>
                        </Grid>
                    </DataTemplate>
                </HierarchicalDataTemplate.ItemTemplate>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Window>

解决方案

使用DataTemplateSelector实现动态控件渲染,这是适配现有ViewModel结构最灵活的方式。

步骤1:创建自定义模板选择器

添加继承自DataTemplateSelector的类,根据ComponentPolicyVM.ValueType返回对应控件模板:

using System;
using System.Windows;
using System.Windows.Controls;
using AppPolicyConfig.ViewModels;

namespace AppPolicyConfig
{
    public class PolicyControlTemplateSelector : DataTemplateSelector
    {
        public DataTemplate BoolTemplate { get; set; }
        public DataTemplate StringTemplate { get; set; }
        public DataTemplate IntTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (item is ComponentPolicyVM policyVM)
            {
                if (policyVM.ValueType == typeof(bool))
                    return BoolTemplate;
                else if (policyVM.ValueType == typeof(string))
                    return StringTemplate;
                else if (policyVM.ValueType == typeof(int))
                    return IntTemplate;
            }
            return base.SelectTemplate(item, container);
        }
    }
}

步骤2:在XAML中定义模板与选择器

修改原XAML,在Window.Resources中定义三种类型的控件模板,再实例化模板选择器并关联这些模板:

<Window x:Class="AppPolicyConfig.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:AppPolicyConfig.ViewModels"
        xmlns:local="clr-namespace:AppPolicyConfig"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <!-- Bool类型控件模板 -->
        <DataTemplate x:Key="BoolTemplate">
            <CheckBox IsChecked="{Binding Value, Mode=TwoWay}" />
        </DataTemplate>

        <!-- String类型控件模板 -->
        <DataTemplate x:Key="StringTemplate">
            <TextBox Text="{Binding Value, Mode=TwoWay}" />
        </DataTemplate>

        <!-- Int类型控件模板(带验证) -->
        <DataTemplate x:Key="IntTemplate">
            <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <TextBox.Style>
                    <Style TargetType="TextBox">
                        <Setter Property="Validation.ErrorTemplate">
                            <Setter.Value>
                                <ControlTemplate>
                                    <DockPanel>
                                        <TextBlock Foreground="Red" FontSize="12" Text="请输入有效整数"/>
                                        <AdornedElementPlaceholder/>
                                    </DockPanel>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                        <Style.Triggers>
                            <Trigger Property="Validation.HasError" Value="True">
                                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TextBox.Style>
                <TextBox.Text>
                    <Binding Path="Value" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
                        <Binding.ValidationRules>
                            <local:IntValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
        </DataTemplate>

        <!-- 自定义模板选择器 -->
        <local:PolicyControlTemplateSelector x:Key="PolicyTemplateSelector"
                                             BoolTemplate="{StaticResource BoolTemplate}"
                                             StringTemplate="{StaticResource StringTemplate}"
                                             IntTemplate="{StaticResource IntTemplate}"/>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainWindowVM/>
    </Window.DataContext>

    <TreeView x:Name="MainTreeView"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Stretch"
              Margin="10"
              ItemsSource="{Binding ComponentPolicyVMs}">
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsExpanded" Value="True" />
            </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding ComponentPolicies}" DataType="{x:Type vm:RolePolicyVM}">
                <TextBlock Text="{Binding Name}"/>
                <HierarchicalDataTemplate.ItemTemplate>
                    <DataTemplate DataType="{x:Type vm:ComponentPolicyVM}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="300" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" 
                                       Text="{Binding Description}"
                                       ToolTip="{Binding ToolTipText}"/>
                            <!-- 使用模板选择器动态加载控件 -->
                            <ContentControl Grid.Column="1"
                                            Content="{Binding}"
                                            ContentTemplateSelector="{StaticResource PolicyTemplateSelector}"/>
                        </Grid>
                    </DataTemplate>
                </HierarchicalDataTemplate.ItemTemplate>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Window>

步骤3:添加整数验证规则(可选)

为避免非整数输入导致绑定错误,添加验证规则类:

using System.Globalization;
using System.Windows.Controls;

namespace AppPolicyConfig
{
    public class IntValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            if (int.TryParse(value.ToString(), out _))
                return ValidationResult.ValidResult;
            else
                return new ValidationResult(false, "输入不是有效整数");
        }
    }
}

核心说明

  • 模板选择器会自动匹配ComponentPolicyVM.ValueType对应的控件模板
  • 所有控件使用双向绑定,确保ViewModel的Value属性与控件值同步
  • 整数输入框添加了验证逻辑,提升输入体验

内容的提问来源于stack exchange,提问作者Mike Bruno

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 10:44:56