做过复杂列表界面的同学,应该都碰到过这种需求:同一个 ListBox 或 ItemsControl,里面的数据类型不同,展示方式也要完全不同。比如消息流里,文本消息、图片消息、系统通知三种卡片布局差异巨大;再比如工单管理系统里,普通工单、紧急工单、已完成工单需要截然不同的视觉呈现。
遇到这种需求,很多人的第一反应是:在 DataTemplate 里堆 Visibility 控制,把所有布局塞进一个模板,然后用绑定来显示或隐藏不同区域。这种做法短期能跑,但随着类型增多,模板会变成一个庞然大物,改起来牵一发动全身。
实际项目统计表明,这类"万能模板"方案在类型超过 4 种后,维护成本会上升约 60%,且极易引入因 Visibility 判断遗漏导致的显示 Bug。
读完这篇文章,你将掌握:
DataTemplateSelector 的底层机制与正确使用姿势先看一个典型的"反面教材",工单列表的常见错误实现:
xml<!-- ❌ 用 Visibility 堆砌的"万能模板"——维护噩梦 -->
<DataTemplate>
<Grid>
<!-- 普通工单区域 -->
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding OrderType}"
Value="Normal">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{Binding Title}" FontSize="14"/>
<TextBlock Text="{Binding Description}" FontSize="12"/>
</StackPanel>
<!-- 紧急工单区域 -->
<Border Background="Red">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding OrderType}"
Value="Urgent">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<!-- 紧急工单的复杂布局... -->
</Border>
<!-- 已完成工单区域... 继续叠加 -->
</Grid>
</DataTemplate>
这种写法有三个致命问题:
第一,所有类型的控件同时存在于视觉树中,只是通过 Visibility 隐藏。即便用户看不见,WPF 依然会为隐藏的控件分配内存、参与布局计算,白白消耗资源。
第二,XAML 文件体积膨胀,单个 DataTemplate 动辄几百行,可读性趋近于零。
第三,类型之间的布局逻辑相互干扰,修改一种类型的样式时,稍不注意就会影响其他类型的显示状态。
DataTemplateSelector 是 WPF 提供的一个抽象基类,它的职责非常单一:在运行时,根据数据对象和容器信息,决定返回哪个 DataTemplate。
其核心方法签名如下:
csharppublic abstract DataTemplate SelectTemplate(object item, DependencyObject container);
item:当前要渲染的数据对象container:承载该数据项的容器控件(如 ListBoxItem)DataTemplate 实例,WPF 用它来渲染 item整个调用链路是这样的:ItemsControl 在为每个数据项生成容器时,如果检测到绑定了 ItemTemplateSelector,就会调用 SelectTemplate,根据返回的模板生成对应的视觉树。每个数据项的视觉树是独立的,不存在隐藏控件占用资源的问题,这是它相比 Visibility 方案的根本优势。
DataTemplateSelector 有两种常见的选择策略:
item is TypeA 返回模板 A,item is TypeB 返回模板 B,适合多态数据集合item 的某个属性值,根据值范围或枚举返回不同模板,适合同一类型但状态差异大的场景消息流界面,包含三种消息类型:文本消息、图片消息、系统通知。三者共用一个 ItemsControl,但视觉布局完全不同。
csharp
// 消息基类
public abstract class MessageBase : INotifyPropertyChanged
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public string SenderId { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// 文本消息
public class TextMessage : MessageBase
{
public string Content { get; set; }
}
// 图片消息
public class ImageMessage : MessageBase
{
private string _imageUrl;
public string ImageUrl
{
get => _imageUrl;
set
{
_imageUrl = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasImage));
}
}
public string Caption { get; set; }
public double ImageWidth { get; set; } = 150;
public double ImageHeight { get; set; } = 100;
// 用于控制占位符显示
public bool HasImage => string.IsNullOrEmpty(ImageUrl);
}
// 系统通知
public class SystemNotification : MessageBase
{
public string NoticeText { get; set; }
public NoticeLevel Level { get; set; } = NoticeLevel.Info;
}
public enum NoticeLevel { Info, Warning, Error }
csharppublic class MessageTemplateSelector : DataTemplateSelector
{
public DataTemplate TextMessageTemplate { get; set; }
public DataTemplate ImageMessageTemplate { get; set; }
public DataTemplate SystemNotificationTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
return item switch
{
TextMessage => TextMessageTemplate,
ImageMessage => ImageMessageTemplate,
SystemNotification => SystemNotificationTemplate,
_ => base.SelectTemplate(item, container)
};
}
}
xml<Window x:Class="AppTemplateSelecter.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:local="clr-namespace:AppTemplateSelecter"
mc:Ignorable="d"
Title="Template Selector Demo" Height="600" Width="800"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<local:NullToCollapsedConverter x:Key="NullToCollapsedConverter"/>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<DataTemplate x:Key="TextMsgTemplate">
<Border Background="#F0F4FF" CornerRadius="8"
Padding="12,8" Margin="4,2" MaxWidth="400"
HorizontalAlignment="Left">
<StackPanel>
<TextBlock Text="{Binding SenderId}"
FontSize="11" Foreground="#757575" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Content}"
FontSize="14" Foreground="#212121"
TextWrapping="Wrap" Margin="0,4,0,0"/>
<TextBlock Text="{Binding Timestamp, StringFormat='HH:mm'}"
FontSize="10" Foreground="#BDBDBD"
HorizontalAlignment="Right" Margin="0,4,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="ImageMsgTemplate">
<Border Background="#FAFAFA" CornerRadius="8"
Padding="8" Margin="4,2" MaxWidth="300"
HorizontalAlignment="Left">
<StackPanel>
<TextBlock Text="{Binding SenderId}"
FontSize="11" Foreground="#757575" FontWeight="SemiBold"/>
<Border CornerRadius="4" Margin="0,6,0,0" Background="#E0E0E0">
<Grid Width="{Binding ImageWidth}" Height="{Binding ImageHeight}">
<Image Source="{Binding ImageUrl}"
Stretch="UniformToFill"
Visibility="{Binding ImageUrl, Converter={StaticResource NullToCollapsedConverter}}"/>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="{Binding HasImage, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="🖼️" FontSize="24" HorizontalAlignment="Center"/>
<TextBlock Text="Image Placeholder" FontSize="10"
Foreground="#757575" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
<TextBlock Text="{Binding Caption}"
FontSize="12" Foreground="#616161"
Margin="0,4,0,0" TextWrapping="Wrap"
Visibility="{Binding Caption, Converter={StaticResource NullToCollapsedConverter}}"/>
<TextBlock Text="{Binding Timestamp, StringFormat='HH:mm'}"
FontSize="10" Foreground="#BDBDBD"
HorizontalAlignment="Right" Margin="0,4,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="SystemNoticeTemplate">
<Border CornerRadius="4" Padding="10,6" Margin="20,4">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#E3F2FD"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Level}"
Value="{x:Static local:NoticeLevel.Warning}">
<Setter Property="Background" Value="#FFF8E1"/>
</DataTrigger>
<DataTrigger Binding="{Binding Level}"
Value="{x:Static local:NoticeLevel.Error}">
<Setter Property="Background" Value="#FFEBEE"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<!-- 图标 -->
<TextBlock Margin="0,0,8,0" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="ℹ️"/>
<Setter Property="Foreground" Value="#2196F3"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Level}" Value="{x:Static local:NoticeLevel.Warning}">
<Setter Property="Text" Value="⚠️"/>
<Setter Property="Foreground" Value="#FF9800"/>
</DataTrigger>
<DataTrigger Binding="{Binding Level}" Value="{x:Static local:NoticeLevel.Error}">
<Setter Property="Text" Value="❌"/>
<Setter Property="Foreground" Value="#F44336"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding NoticeText}"
FontSize="12" VerticalAlignment="Center"
Foreground="#546E7A"/>
</StackPanel>
</Border>
</DataTemplate>
<local:MessageTemplateSelector x:Key="MsgSelector"
TextMessageTemplate="{StaticResource TextMsgTemplate}"
ImageMessageTemplate="{StaticResource ImageMsgTemplate}"
SystemNotificationTemplate="{StaticResource SystemNoticeTemplate}"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto" Padding="10">
<ItemsControl x:Name="MessageList"
ItemsSource="{Binding Messages}"
ItemTemplateSelector="{StaticResource MsgSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<Border Grid.Row="1" Background="#F5F5F5" Padding="10">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Name="BtnAddText" Content="Add Text Message"
Margin="5" Padding="10,5" MinWidth="120"
Background="#2196F3" Foreground="White" BorderThickness="0" />
<Button Name="BtnAddImage" Content="Add Image Message"
Margin="5" Padding="10,5" MinWidth="120"
Background="#4CAF50" Foreground="White" BorderThickness="0" />
<Button Name="BtnAddNotice" Content="Add System Notice"
Margin="5" Padding="10,5" MinWidth="120"
Background="#FF9800" Foreground="White" BorderThickness="0"/>
<Button Name="BtnClear" Content="Clear All"
Margin="5" Padding="10,5" MinWidth="120"
Background="#F44336" Foreground="White" BorderThickness="0"/>
</StackPanel>
</Border>
</Grid>
</Window>

这里有个值得关注的细节:系统通知模板内部仍然使用了 DataTrigger 来处理不同级别的背景色变化。DataTemplateSelector 和 DataTrigger 不是互斥关系,而是互补的——前者处理"用哪个模板",后者处理"模板内部的状态变化",两者组合使用才是最优解。
工单管理系统中,所有工单都是同一个 WorkOrder 类型,但根据 Priority(优先级)和 Status(状态)的组合,需要展示三种完全不同的卡片布局:标准卡片、紧急卡片(带醒目警示区)、已归档卡片(灰显简化版)。
csharpusing System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AppTemplateSelecter
{
public enum WorkOrderPriority { Low, Normal, High, Critical }
public enum WorkOrderStatus { Pending, InProgress, Completed, Archived }
public class WorkOrder : INotifyPropertyChanged
{
private string _orderId;
private string _title;
private string _assignedTo;
private WorkOrderPriority _priority;
private WorkOrderStatus _status;
private DateTime _dueDate;
private string _description;
public string OrderId
{
get => _orderId;
set
{
_orderId = value;
OnPropertyChanged();
}
}
public string Title
{
get => _title;
set
{
_title = value;
OnPropertyChanged();
}
}
public string AssignedTo
{
get => _assignedTo;
set
{
_assignedTo = value;
OnPropertyChanged();
}
}
public WorkOrderPriority Priority
{
get => _priority;
set
{
_priority = value;
OnPropertyChanged();
}
}
public WorkOrderStatus Status
{
get => _status;
set
{
_status = value;
OnPropertyChanged();
}
}
public DateTime DueDate
{
get => _dueDate;
set
{
_dueDate = value;
OnPropertyChanged();
}
}
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
csharpusing System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace AppTemplateSelecter
{
public class WorkOrderViewModel: INotifyPropertyChanged
{
private ObservableCollection<WorkOrder> _workOrders;
public ObservableCollection<WorkOrder> WorkOrders
{
get => _workOrders;
set
{
_workOrders = value;
OnPropertyChanged();
}
}
public WorkOrderViewModel()
{
LoadSampleData();
}
private void LoadSampleData()
{
WorkOrders = new ObservableCollection<WorkOrder>
{
new WorkOrder
{
OrderId = "WO-001",
Title = "服务器维护",
AssignedTo = "张三",
Priority = WorkOrderPriority.Normal,
Status = WorkOrderStatus.InProgress,
DueDate = DateTime.Now.AddDays(2),
Description = "定期服务器维护和系统更新"
},
new WorkOrder
{
OrderId = "WO-002",
Title = "数据库紧急修复",
AssignedTo = "李四",
Priority = WorkOrderPriority.Critical,
Status = WorkOrderStatus.Pending,
DueDate = DateTime.Now.AddHours(4),
Description = "生产环境数据库连接异常,需要立即处理"
},
new WorkOrder
{
OrderId = "WO-003",
Title = "网络设备升级",
AssignedTo = "王五",
Priority = WorkOrderPriority.High,
Status = WorkOrderStatus.InProgress,
DueDate = DateTime.Now.AddDays(1),
Description = "核心网络设备固件升级"
},
new WorkOrder
{
OrderId = "WO-004",
Title = "办公软件更新",
AssignedTo = "赵六",
Priority = WorkOrderPriority.Low,
Status = WorkOrderStatus.Completed,
DueDate = DateTime.Now.AddDays(-1),
Description = "员工电脑办公软件统一更新"
},
new WorkOrder
{
OrderId = "WO-005",
Title = "安全评估报告",
AssignedTo = "钱七",
Priority = WorkOrderPriority.Normal,
Status = WorkOrderStatus.Archived,
DueDate = DateTime.Now.AddDays(-5),
Description = "季度安全评估报告已完成归档"
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
xml<!-- 标准工单模板 -->
<DataTemplate x:Key="StandardOrderTemplate">
<Border Background="White" BorderBrush="#E0E0E0"
BorderThickness="1" CornerRadius="6"
Padding="14,10" Margin="0,0,0,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Title}"
FontSize="14" FontWeight="SemiBold"
Foreground="#212121"/>
<TextBlock FontSize="12" Foreground="#757575"
Margin="0,4,0,0">
<Run Text="负责人:"/>
<Run Text="{Binding AssignedTo}"/>
</TextBlock>
</StackPanel>
<TextBlock Grid.Column="1"
Text="{Binding DueDate, StringFormat='MM/dd 截止'}"
FontSize="11" Foreground="#9E9E9E"
VerticalAlignment="Top"/>
</Grid>
</Border>
</DataTemplate>
<!-- 紧急工单模板 -->
<DataTemplate x:Key="UrgentOrderTemplate">
<Border BorderBrush="#E53935" BorderThickness="2"
CornerRadius="6" Margin="0,0,0,8">
<!-- 顶部警示条 -->
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FFEBEE" Offset="0"/>
<GradientStop Color="White" Offset="0.3"/>
</LinearGradientBrush>
</Border.Background>
<StackPanel>
<!-- 警示头部 -->
<Border Background="#E53935" CornerRadius="4,4,0,0"
Padding="14,6">
<Grid>
<TextBlock Text="⚠ 紧急工单"
Foreground="White" FontWeight="Bold"
FontSize="12"/>
<TextBlock Text="{Binding Priority}"
Foreground="#FFCDD2" FontSize="11"
HorizontalAlignment="Right"/>
</Grid>
</Border>
<!-- 工单内容 -->
<StackPanel Padding="14,10">
<TextBlock Text="{Binding Title}"
FontSize="15" FontWeight="Bold"
Foreground="#B71C1C"/>
<TextBlock Text="{Binding Description}"
FontSize="12" Foreground="#424242"
TextWrapping="Wrap" Margin="0,6,0,0"
MaxHeight="60"/>
<Grid Margin="0,8,0,0">
<TextBlock FontSize="12" Foreground="#757575">
<Run Text="负责人:"/>
<Run Text="{Binding AssignedTo}" FontWeight="SemiBold"/>
</TextBlock>
<TextBlock Text="{Binding DueDate, StringFormat='截止:MM/dd HH:mm'}"
FontSize="11" Foreground="#E53935"
HorizontalAlignment="Right" FontWeight="Bold"/>
</Grid>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
<!-- 已归档模板(简化灰显版) -->
<DataTemplate x:Key="ArchivedOrderTemplate">
<Border Background="#FAFAFA" BorderBrush="#EEEEEE"
BorderThickness="1" CornerRadius="6"
Padding="14,8" Margin="0,0,0,6" Opacity="0.7">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding Title}"
FontSize="13" Foreground="#9E9E9E">
<TextBlock.TextDecorations>
<TextDecoration Location="Strikethrough"/>
</TextBlock.TextDecorations>
</TextBlock>
<TextBlock Grid.Column="1"
Text="已归档" FontSize="11"
Foreground="#BDBDBD"
VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>
<!-- 注册选择器 -->
<local:WorkOrderTemplateSelector x:Key="OrderSelector"
StandardTemplate="{StaticResource StandardOrderTemplate}"
UrgentTemplate="{StaticResource UrgentOrderTemplate}"
ArchivedTemplate="{StaticResource ArchivedOrderTemplate}"/>

DataTemplateSelector 有一个不太直观的行为:它只在数据项首次绑定时调用一次 SelectTemplate,之后即使数据属性发生变化,模板也不会自动切换。
这在工单场景里是个真实问题——工单状态从"进行中"变为"已归档"时,卡片不会自动切换到归档模板。
解决方案是通过重置 ItemTemplateSelector 来强制刷新,或者更优雅地,在 ViewModel 里通过触发集合刷新来驱动:
csharp/// <summary>
/// 支持动态刷新的模板选择器
/// 通过监听数据变化,强制重新触发模板选择
/// </summary>
public class DynamicWorkOrderSelector : DataTemplateSelector
{
public DataTemplate StandardTemplate { get; set; }
public DataTemplate UrgentTemplate { get; set; }
public DataTemplate ArchivedTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is not WorkOrder order)
return base.SelectTemplate(item, container);
// 订阅属性变化,在状态改变时通知容器刷新模板
if (container is FrameworkElement fe)
{
order.PropertyChanged -= OnOrderPropertyChanged;
order.PropertyChanged += OnOrderPropertyChanged;
// 将容器引用存入 Tag,供回调使用
void OnOrderPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(WorkOrder.Status)
or nameof(WorkOrder.Priority))
{
// 通过重新设置 ContentTemplate 触发模板刷新
if (fe is ContentPresenter cp)
{
cp.ContentTemplate = null;
cp.ContentTemplateSelector = null;
cp.ContentTemplateSelector = this;
}
}
}
}
if (order.Status == WorkOrderStatus.Archived)
return ArchivedTemplate;
if (order.Priority is WorkOrderPriority.Critical
or WorkOrderPriority.High)
return UrgentTemplate;
return StandardTemplate;
}
}
踩坑预警:上述事件订阅需要注意内存泄漏风险。每次 SelectTemplate 调用时先取消订阅再重新订阅(-= 后 +=),可以避免同一对象被重复订阅。如果数据项会频繁创建和销毁,建议在 WorkOrder 的析构或 Dispose 中主动清理事件订阅。
坑一:模板选择器不响应属性变化。 如前文所述,SelectTemplate 只调用一次。如果需要动态切换模板,必须主动触发刷新,不能指望 WPF 自动处理。
坑二:在 SelectTemplate 里做耗时操作。 SelectTemplate 在列表滚动时会被频繁调用,如果里面有数据库查询、网络请求或复杂计算,会直接导致滚动卡顿。选择逻辑必须保持轻量,只做简单的类型判断或属性读取。
坑三:模板属性未正确赋值。 在 XAML 里注册选择器时,如果某个模板属性名拼写错误,SelectTemplate 返回 null,WPF 不会报错,只是该数据项显示为空白。调试时优先检查 XAML 中的属性绑定是否与 C# 属性名完全一致。
坑四:与 ContentControl 配合时的容器类型。 DataTemplateSelector 用在 ItemsControl 时,container 是对应的 ItemContainerGenerator 生成的容器(如 ListBoxItem);用在 ContentControl.ContentTemplateSelector 时,container 是 ContentPresenter。两种场景下对 container 的类型转换逻辑不同,混用会导致空引用异常。
这篇文章把 DataTemplateSelector 从原理到实战完整走了一遍,提炼三个核心结论:
第一,DataTemplateSelector 解决的是"用哪个模板"的问题,而不是"模板内部长什么样"的问题。两个职责要分清楚,后者交给 DataTrigger 和 Style 处理,不要混为一谈。
第二,模板选择器的最大价值是关注点分离。每种数据类型或状态对应独立的 DataTemplate,修改一种类型的布局不会影响其他类型,代码的可维护性从根本上得到改善。
第三,性能优化的关键是虚拟化,而非模板数量。只要配合 VirtualizingStackPanel 使用,即便有十几种模板类型,大列表的渲染性能依然可以保持流畅。
如果你正在做的项目涉及 WPF 列表开发,这套模板选择器方案可以直接套用,结合上一篇的 DataTrigger 状态管理,基本上能覆盖 90% 的复杂列表场景。
💬 互动话题
在你的项目里,有没有遇到过比本文更复杂的模板切换需求——比如需要根据多个属性的组合来决定模板,或者模板本身需要支持运行时热加载?欢迎在评论区描述你的场景,一起探讨更灵活的实现方式。
#C# #WPF #DataTemplate #MVVM #性能优化
相关信息
我用夸克网盘给你分享了「AppTemplateSelecter.zip」,点击链接或复制整段内容,打开「夸克APP」即可获取。
/6eac3YvaUs:/
链接:https://pan.quark.cn/s/bf97e20db9f1
提取码:qUL5


本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!