WPF DataGrid虚拟化滚动后文本显示异常问题求助
问题描述
场景
使用开启VirtualizingPanel.IsVirtualizing="true"的DataGrid和搜索栏,输入搜索内容后列表自动过滤,通过附加属性实现搜索文本高亮——将TextBlock文本拆分为多个Inline,匹配搜索文本的Inline设置黄色背景,该高亮功能本身正常。
异常现象
向下滚动再向上滚动时,离开过可视区域的元素文本会变成其他元素的内容,列表末尾也会出现该问题,再次滚动后部分文本又恢复正常。
排查结果
- DataGrid的
LoadingRow事件中,进入可视区域的Row的DataContext数据正确,但TextBlock的文本未更新。 - 在
HighlightTextBlock方法开头,txtBlock.GetBindingExpression(TextBlock.TextProperty)能返回有效绑定,但设置完Inline后返回null,推测操作Inline时破坏了Text属性的绑定。 - 关闭
EnableRowVirtualization="false"后功能正常,但列表最多有5000条数据,虚拟化必须保留。
测试代码
Formular.xaml
<DataGrid Grid.Row="0" local:Highlighter.Filter="{Binding Filter, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="true" ColumnWidth="100" ItemsSource="{Binding Path=DisplayedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" RowHeight="30" SelectionMode="Single" /> <WrapPanel Grid.Row="1"> <Label Content="Filter: " /> <TextBox Width="100" Text="{Binding Path=Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </WrapPanel>
Formular.xaml.cs
public partial class Formular : INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { }; public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion public ICollectionView DisplayedItems { get; set; } private string filter; public string Filter { get => this.filter; set { this.filter = value; this.DisplayedItems.Refresh(); this.RaisePropertyChanged(); } } public Formular() { InitializeComponent(); this.DataContext = this; var listItems = new ObservableCollection<MyListItem>() { new MyListItem("Alpha", "Mission1"), new MyListItem("Beta1", "Mission1"), new MyListItem("Beta1", "Mission2"), new MyListItem("Beta1", "Mission3"), new MyListItem("Beta1", "Mission4"), new MyListItem("Beta1", "Mission5"), new MyListItem("Beta1", "Mission6"), new MyListItem("Beta1", "Mission7"), new MyListItem("Beta1", "Mission8"), new MyListItem("Beta1", "Mission9"), new MyListItem("Beta2", "Mission2"), }; this.DisplayedItems = CollectionViewSource.GetDefaultView(listItems); this.DisplayedItems.Filter = this.FilterCallback; } public bool FilterCallback(object obj) { var item = (MyListItem) obj; return string.IsNullOrEmpty(this.Filter) || item.Name.ToUpper().Contains(Filter.ToUpper()) || item.MissionName.ToUpper().Contains(Filter.ToUpper()); } }
Highlighter.cs
public static class Highlighter { private static string filter; static Highlighter(){} #region Filter public static readonly DependencyProperty FilterProperty = DependencyProperty.RegisterAttached("Filter", typeof(string), typeof(Highlighter), new PropertyMetadata("", PropertyChangedCallback)); public static void SetFilter(DependencyObject obj, string value) { obj.SetValue(FilterProperty, value); } public static string GetFilter(DependencyObject obj) { return (string)obj?.GetValue(FilterProperty); } private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => DoAction(d))); } #endregion private static void DoAction(DependencyObject d) { filter = GetFilter(d); if (filter == null) { return; } var grid = (DataGrid)d; grid.LoadingRow += GridOnLoadingRow; // Get DataGridRows var gridRows = grid.GetDescendants<DataGridRow>().ToList(); foreach (var row in gridRows) { HighlightRow(row); } } private static void HighlightRow(DataGridRow row) { // Get TextBlocks var txtBlocks = row.GetDescendants<TextBlock>().ToList(); if (!txtBlocks.Any()) { return; } foreach (var txtBlock in txtBlocks) { HighlightTextBlock(txtBlock); } } private static void HighlightTextBlock(TextBlock txtBlock) { var text = txtBlock.Text; if (string.IsNullOrEmpty(text)) { return; } // Check whether the text contains the filter text var index = text.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase); if (index < 0) { // Filter text not found return; } // Generate Inlines with highlighting information var inlines = new List<Inline>(); while (true) { // Text from beginning to filter text inlines.Add(new Run(text.Substring(0, index))); // Text that corresponds to the filter text inlines.Add(new Run(text.Substring(index, filter.Length)) { Background = Brushes.Yellow }); // Text from filter text to ending text = text.Substring(index + filter.Length); // Check whether the remaining text also contains the filter text index = text.IndexOf(filter, StringComparison.CurrentCultureIgnoreCase); if (index < 0) { // If not, add remaining text and exit loop inlines.Add(new Run(text)); break; } } // Replace Inlines txtBlock.Inlines.Clear(); txtBlock.Inlines.AddRange(inlines); } private static void GridOnLoadingRow(object sender, DataGridRowEventArgs e) { var dataContext = (MyListItem) e.Row.DataContext; var newData = $"{dataContext.Name}_{dataContext.MissionName}"; var oldData = string.Join("_", e.Row.GetDescendants<TextBlock>().Select(t => t.Text).ToList()); } }
MyListItem.cs
public class MyListItem : INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { }; public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion public string name; public string Name { get => name; set { this.name = value; this.RaisePropertyChanged(); } } public string missionName; public string MissionName { get => missionName; set { this.missionName = value; this.RaisePropertyChanged(); } } public MyListItem(string name, string missionName) { this.Name = name; this.MissionName = missionName; } }
解决方案
核心问题分析
- 绑定破坏:手动修改
TextBlock.Inlines会清除Text属性的绑定——WPF中Text和Inlines互斥,设置其中一个会清空另一个的内容及绑定关系。 - 行复用冲突:DataGrid虚拟化时会复用行容器,被复用的行的TextBlock已失去绑定,无法自动更新为新DataContext的文本,导致显示旧内容。
- 事件重复订阅:每次Filter变化时都会重新订阅
LoadingRow事件,导致同一事件被多次触发,引发异常。
修改步骤
1. 修复绑定丢失问题
在修改Inlines前保存原始绑定,处理后根据情况恢复绑定,确保Text能从DataContext获取最新值:
private static void HighlightTextBlock(TextBlock txtBlock, string currentFilter) { // 保存原始绑定表达式 var textBinding = txtBlock.GetBindingExpression(TextBlock.TextProperty); if (textBinding == null) return; // 强制更新绑定,获取最新文本 textBinding.UpdateTarget(); var text = txtBlock.Text; if (string.IsNullOrEmpty(text) || text.IndexOf(currentFilter, StringComparison.CurrentCultureIgnoreCase) < 0) { // 无文本或无匹配时,恢复原始绑定并清空Inlines txtBlock.Inlines.Clear(); txtBlock.SetBinding(TextBlock.TextProperty, textBinding.ParentBinding); return; } // 生成高亮Inlines var inlines = new List<Inline>(); int index; while ((index = text.IndexOf(currentFilter, StringComparison.CurrentCultureIgnoreCase)) >= 0) { inlines.Add(new Run(text.Substring(0, index))); inlines.Add(new Run(text.Substring(index, currentFilter.Length)) { Background = Brushes.Yellow }); text = text.Substring(index + currentFilter.Length); } inlines.Add(new Run(text)); // 替换Inlines txtBlock.Inlines.Clear(); txtBlock.Inlines.AddRange(inlines); }
2. 修正事件订阅逻辑
避免重复订阅LoadingRow事件,改为每次Filter变化时先移除旧处理再添加新处理:
private static void DoAction(DependencyObject d) { var grid = (DataGrid)d; var currentFilter = GetFilter(d); // 先移除旧事件处理,防止重复订阅 grid.LoadingRow -= GridOnLoadingRowHandler; grid.LoadingRow += (s, e) => GridOnLoadingRowHandler(s, e, currentFilter); // 高亮当前可见行 foreach (var row in grid.GetDescendants<DataGridRow>().ToList()) { HighlightRow(row, currentFilter); } } private static void HighlightRow(DataGridRow row, string currentFilter) { foreach (var txtBlock in row.GetDescendants<TextBlock>().ToList()) { HighlightTextBlock(txtBlock, currentFilter); } } private static void GridOnLoadingRowHandler(object sender, DataGridRowEventArgs e, string currentFilter) { HighlightRow(e.Row, currentFilter); }
3. 移除静态全局Filter变量
删除静态filter字段,改为每次传递当前控件的Filter值,避免多DataGrid实例共享过滤值的冲突。
4. 明确虚拟化配置(可选)
在DataGrid的XAML中显式设置虚拟化参数,确保行为一致:
<DataGrid ... VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" />
内容的提问来源于stack exchange,提问作者AnnaBauer21
相关产品推荐
相关产品推荐

