一、前言:WPF 大数据列表的通用痛点
在 WPF 项目中,直接给 DataGrid / ListBox 绑定几万、十几万条数据,会出现严重问题:
-
UI 卡死、首次渲染极慢
-
控件初始化阻塞、窗口卡顿
-
原生虚拟化在复杂模板、分组、列自适应场景下失效
-
业务层需要全量加载数据,无法做到滚动按需渲染
行业常规方案:前端分页 / 后端分页,但很多桌面端业务需要全量拿到内存、前端滚动懒加载。
为此,我们从零实现了一套纯前端、无依赖、高性能、可复用的懒加载控件:
-
LazyDataGrid:适配 DataGrid 表格场景
-
LazyListBox:适配 ListBox 列表场景
彻底解决 WPF 大数据列表卡顿、全量渲染卡死、二次刷新失效等经典问题。
二、技术演进复盘(关键踩坑记录)
本次最终版本经历了 3 轮架构迭代,每一轮都解决一个底层 WPF 绑定坑,非常值得总结。
1)初代方案:附加属性 AttachedProperty 懒加载
最初采用静态附加属性实现懒加载逻辑,优点是侵入低、无需自定义控件。
致命问题(WPF 底层BUG):
附加属性绑定,在数据源整体替换 new ObservableCollection 时,不会自动触发 DP 变更回调。
现象:
-
第一次绑定数据正常
-
第二次赋值新集合,PropertyChanged 触发,但附加属性 DP 不更新
原因:WPF 附加属性绑定的更新优先级低于普通 DP,非可视化树对象绑定存在通知丢失。
2)迭代方案:Behavior 行为封装
将逻辑迁移到 Behavior<Control>,结构更优雅、生命周期更规范。
依然存在同款坑:
Behavior 属于 Freezable 对象,不在可视化树内,DP 绑定变更通知同样会丢失。
二次替换数据源依然不刷新,无法根治。
3)最终稳定方案:继承原生控件 + 普通 DependencyProperty
彻底放弃附加属性、放弃 Behavior 绑定,采用自定义控件继承原生控件的架构:
-
LazyDataGrid : DataGrid
-
LazyListBox : ListBox
核心优势:自定义控件属于可视化树原生控件,注册的普通 DependencyProperty 100% 正常触发变更回调,无论多少次替换数据源,都能自动 Reset 刷新。至此,所有二次更新失效、绑定不刷新问题彻底根治。
三、控件整体架构设计
1)核心设计思想
数据双层分离,从根源解决大数据渲染卡顿:
-
全量数据源 AllItems:ViewModel 绑定的完整原始集合(几万/十几万条)
-
可视数据源 VisibleItems:控件内部子集,真正绑定到 ItemsSource 用于UI渲染
UI 永远只渲染可视区域 + 预加载区域,杜绝一次性渲染全部数据,极致优化渲染性能。
2)核心能力
-
自动查找控件内部 ScrollViewer,无需手动适配模板
-
自动测量行高/项高,无需手动固定高度,适配任意ItemTemplate
-
根据窗口视口大小动态计算每页数据量,自适应窗口缩放
-
支持自定义预加载屏幕倍数,平衡流畅度与内存占用
-
滚动到底部自动续页,无感懒加载
-
数据源整体替换自动重置分页,无需手动刷新
-
控件尺寸变化自动重算分页,适配窗口缩放
-
完整生命周期事件解绑,杜绝内存泄漏
3)可配置参数说明
|
IsEnabled |
bool |
开启/关闭懒加载,关闭后完全恢复原生控件渲染逻辑 |
|
PreloadScreen |
double |
预加载屏幕倍数,默认1.2倍可视区域,数值越大预加载越多 |
|
AllItems |
IList |
ViewModel绑定的全量原始数据源,支持任意IList集合 |
四、控件工作流程
控件加载完成:自动遍历可视化树查找内部 ScrollViewer,绑定滚动监听事件
容器生成完成:自动测量首行/首项真实高度,适配自定义模板
动态计算分页:通过视口高度、单项高度、预加载倍数计算单页加载量
首次初始化:加载第一屏数据,完成初始渲染
滚动监听加载:滚动距离底部不足半屏,自动加载下一页数据
数据源变更响应:替换AllItems数据源时,自动清空旧数据、重置分页、重新加载
五、两款控件区别与适用场景
1)LazyDataGrid
-
继承自原生 DataGrid,保留所有表格原生特性
-
自动测量 DataGridRow 行高,适配多列表格场景
-
适用场景:多列数据展示、需要表头、排序、筛选、单元格交互的业务表格
2)LazyListBox
-
继承自原生 ListBox,保留所有列表原生特性
-
自动测量 ListBoxItem 项高,适配任意自定义ItemTemplate
-
适用场景:单行列表、日志列表、消息列表、简单条目流式展示
共同核心特性
-
完全保留原生控件所有功能:模板、样式、选中、交互、事件
-
绑定稳定,彻底解决附加属性/Behavior绑定失效玄学问题
-
支持十万级大数据流畅滚动,无卡顿、无卡死
六、完整源码(可直接落地使用)
6.1 公共实体模型 ItemModel
/// <summary>
/// 通用列表测试实体模型
/// </summary>
public class ItemModel
{
public int Id { get; set; }
public string Name { get; set; }
}
6.2 标准 MVVM ViewModel
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
/// <summary>
/// 通用懒加载列表ViewModel,纯MVVM无冗余代码
/// </summary>
public class LazyListViewModel : INotifyPropertyChanged
{
private ObservableCollection<ItemModel> _items;
/// <summary>
/// 全量绑定数据源,支持多次替换刷新
/// </summary>
public ObservableCollection<ItemModel> Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
6.3 LazyDataGrid 完整源码
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WpfAttachedLazyLoad
{
public class LazyDataGrid : DataGrid
{
#region DependencyProperties
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.Register(
nameof(IsEnabled),
typeof(bool),
typeof(LazyDataGrid),
new PropertyMetadata(false, OnIsEnabledChanged));
public bool IsEnabled
{
get => (bool)GetValue(IsEnabledProperty);
set => SetValue(IsEnabledProperty, value);
}
public static readonly DependencyProperty PreloadScreenProperty =
DependencyProperty.Register(
nameof(PreloadScreen),
typeof(double),
typeof(LazyDataGrid),
new PropertyMetadata(1.2));
public double PreloadScreen
{
get => (double)GetValue(PreloadScreenProperty);
set => SetValue(PreloadScreenProperty, value);
}
/// <summary>完整原始数据源,ViewModel绑定入口</summary>
public static readonly DependencyProperty AllItemsProperty =
DependencyProperty.Register(
nameof(AllItems),
typeof(IList),
typeof(LazyDataGrid),
new PropertyMetadata(null, OnAllItemsChanged));
public IList AllItems
{
get => (IList)GetValue(AllItemsProperty);
set => SetValue(AllItemsProperty, value);
}
/// <summary>内部可视子集,真正绑定到ItemsSource用于渲染</summary>
private static readonly DependencyProperty VisibleItemsProperty =
DependencyProperty.Register(
nameof(VisibleItems),
typeof(ObservableCollection<object>),
typeof(LazyDataGrid),
new PropertyMetadata(null));
private ObservableCollection<object> VisibleItems
{
get => (ObservableCollection<object>)GetValue(VisibleItemsProperty);
set => SetValue(VisibleItemsProperty, value);
}
#endregion
#region Private Fields & 上下文
private LazyLoadContext _lazyCtx;
private ScrollViewer _scrollViewer;
/// <summary>懒加载上下文缓存</summary>
private class LazyLoadContext
{
public int PageSize { get; set; }
public int LoadedItemIndex { get; set; }
public bool IsLoading { get; set; }
public double MeasuredRowHeight { get; set; }
}
#endregion
#region 依赖属性变更回调
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as LazyDataGrid;
bool enable = (bool)e.NewValue;
if (enable)
grid.AttachLazy();
else
grid.DetachLazy();
}
private static void OnAllItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as LazyDataGrid;
if (!grid.IsEnabled) return;
// 数据源整体替换,自动重置分页
grid.Reset();
}
#endregion
#region 懒加载挂载/卸载生命周期
private void AttachLazy()
{
if (_lazyCtx != null) return;
_lazyCtx = new LazyLoadContext();
VisibleItems = new ObservableCollection<object>();
ItemsSource = VisibleItems;
// 绑定生命周期事件
Loaded += Host_Loaded;
Unloaded += Host_Unloaded;
SizeChanged += Host_SizeChanged;
ItemContainerGenerator.StatusChanged += Generator_StatusChanged;
}
private void DetachLazy()
{
// 解绑滚动事件,防止内存泄漏
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
_scrollViewer = null;
}
// 解绑所有生命周期事件
Loaded -= Host_Loaded;
Unloaded -= Host_Unloaded;
SizeChanged -= Host_SizeChanged;
ItemContainerGenerator.StatusChanged -= Generator_StatusChanged;
_lazyCtx = null;
VisibleItems = null;
ItemsSource = null;
}
#endregion
#region 递归查找内部ScrollViewer
private ScrollViewer FindScrollViewer(DependencyObject parent)
{
if (parent == null) return null;
if (parent is ScrollViewer sv) return sv;
int childCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
var result = FindScrollViewer(child);
if (result != null) return result;
}
return null;
}
#endregion
#region 控件生命周期事件
private void Host_Loaded(object sender, RoutedEventArgs e)
{
_scrollViewer = FindScrollViewer(this);
if (_scrollViewer != null)
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
CalcPageSize();
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(Reset));
}
private void Host_Unloaded(object sender, RoutedEventArgs e)
{
if (_scrollViewer != null)
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
private void Host_SizeChanged(object sender, SizeChangedEventArgs e)
{
CalcPageSize();
Reset();
}
private void Generator_StatusChanged(object sender, EventArgs e)
{
var gen = sender as ItemContainerGenerator;
if (gen == null || gen.Status != GeneratorStatus.ContainersGenerated)
return;
if (_lazyCtx == null || _lazyCtx.MeasuredRowHeight > 0)
return;
// 自动测量首行高度
var firstRow = gen.ContainerFromIndex(0) as DataGridRow;
if (firstRow == null) return;
double rowHeight = firstRow.ActualHeight;
if (rowHeight > 0)
{
_lazyCtx.MeasuredRowHeight = rowHeight;
CalcPageSize();
Reset();
}
}
#endregion
#region 动态计算分页大小
private void CalcPageSize()
{
double preload = PreloadScreen;
double rowHeight = _lazyCtx.MeasuredRowHeight > 0 ? _lazyCtx.MeasuredRowHeight : 30;
if (_scrollViewer == null || _scrollViewer.ViewportHeight <= 0)
{
_lazyCtx.PageSize = 20;
return;
}
// 根据视口高度动态计算单页数据量
int oneScreen = (int)Math.Ceiling(_scrollViewer.ViewportHeight / rowHeight);
_lazyCtx.PageSize = (int)Math.Ceiling(oneScreen * preload);
}
#endregion
#region 加载下一页数据
private void LoadNextPage()
{
if (_lazyCtx.IsLoading) return;
IList source = AllItems;
var visible = VisibleItems;
if (source == null || visible == null || _lazyCtx.LoadedItemIndex >= source.Count)
return;
_lazyCtx.IsLoading = true;
try
{
int take = Math.Min(_lazyCtx.PageSize, source.Count – _lazyCtx.LoadedItemIndex);
for (int i = 0; i < take; i++)
{
visible.Add(source[_lazyCtx.LoadedItemIndex + i]);
}
_lazyCtx.LoadedItemIndex += take;
}
finally
{
_lazyCtx.IsLoading = false;
}
}
#endregion
#region 滚动预加载逻辑
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_lazyCtx == null || _lazyCtx.IsLoading) return;
CalcPageSize();
IList source = AllItems;
if (source == null || _lazyCtx.LoadedItemIndex >= source.Count) return;
double rowHeight = _lazyCtx.MeasuredRowHeight > 0 ? _lazyCtx.MeasuredRowHeight : 30;
double remain = _scrollViewer.ExtentHeight – (_scrollViewer.VerticalOffset + _scrollViewer.ViewportHeight);
// 距离底部小于半行高度,触发预加载
if (remain < rowHeight * 0.5)
{
LoadNextPage();
}
}
#endregion
#region 对外公开重置方法
public void Reset()
{
if (_lazyCtx == null || VisibleItems == null) return;
IList source = AllItems;
if (source == null) return;
VisibleItems.Clear();
_lazyCtx.LoadedItemIndex = 0;
_lazyCtx.IsLoading = false;
LoadNextPage();
}
#endregion
}
}
6.4 LazyListBox 完整源码
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WpfAttachedLazyLoad
{
public class LazyListBox : ListBox
{
#region DependencyProperties
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.Register(
nameof(IsEnabled),
typeof(bool),
typeof(LazyListBox),
new PropertyMetadata(false, OnIsEnabledChanged));
public bool IsEnabled
{
get => (bool)GetValue(IsEnabledProperty);
set => SetValue(IsEnabledProperty, value);
}
public static readonly DependencyProperty PreloadScreenProperty =
DependencyProperty.Register(
nameof(PreloadScreen),
typeof(double),
typeof(LazyListBox),
new PropertyMetadata(1.2));
public double PreloadScreen
{
get => (double)GetValue(PreloadScreenProperty);
set => SetValue(PreloadScreenProperty, value);
}
/// <summary>完整原始数据源,ViewModel绑定入口</summary>
public static readonly DependencyProperty AllItemsProperty =
DependencyProperty.Register(
nameof(AllItems),
typeof(IList),
typeof(LazyListBox),
new PropertyMetadata(null, OnAllItemsChanged));
public IList AllItems
{
get => (IList)GetValue(AllItemsProperty);
set => SetValue(AllItemsProperty, value);
}
/// <summary>内部可视子集,真正绑定到ItemsSource用于渲染</summary>
private static readonly DependencyProperty VisibleItemsProperty =
DependencyProperty.Register(
nameof(VisibleItems),
typeof(ObservableCollection<object>),
typeof(LazyListBox),
new PropertyMetadata(null));
private ObservableCollection<object> VisibleItems
{
get => (ObservableCollection<object>)GetValue(VisibleItemsProperty);
set => SetValue(VisibleItemsProperty, value);
}
#endregion
#region Private Fields & 上下文
private LazyLoadContext _lazyCtx;
private ScrollViewer _scrollViewer;
/// <summary>懒加载上下文缓存</summary>
private class LazyLoadContext
{
public int PageSize { get; set; }
public int LoadedItemIndex { get; set; }
public bool IsLoading { get; set; }
public double MeasuredItemHeight { get; set; }
}
#endregion
#region 依赖属性变更回调
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listBox = d as LazyListBox;
bool enable = (bool)e.NewValue;
if (enable)
listBox.AttachLazy();
else
listBox.DetachLazy();
}
private static void OnAllItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listBox = d as LazyListBox;
if (!listBox.IsEnabled) return;
// 数据源整体替换,自动重置分页
listBox.Reset();
}
#endregion
#region 懒加载挂载/卸载生命周期
private void AttachLazy()
{
if (_lazyCtx != null) return;
_lazyCtx = new LazyLoadContext();
VisibleItems = new ObservableCollection<object>();
ItemsSource = VisibleItems;
// 绑定生命周期事件
Loaded += Host_Loaded;
Unloaded += Host_Unloaded;
SizeChanged += Host_SizeChanged;
ItemContainerGenerator.StatusChanged += Generator_StatusChanged;
}
private void DetachLazy()
{
// 解绑滚动事件,防止内存泄漏
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
_scrollViewer = null;
}
// 解绑所有生命周期事件
Loaded -= Host_Loaded;
Unloaded -= Host_Unloaded;
SizeChanged -= Host_SizeChanged;
ItemContainerGenerator.StatusChanged -= Generator_StatusChanged;
_lazyCtx = null;
VisibleItems = null;
ItemsSource = null;
}
#endregion
#region 递归查找内部ScrollViewer
private ScrollViewer FindScrollViewer(DependencyObject parent)
{
if (parent == null) return null;
if (parent is ScrollViewer sv) return sv;
int childCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
var result = FindScrollViewer(child);
if (result != null) return result;
}
return null;
}
#endregion
#region 控件生命周期事件
private void Host_Loaded(object sender, RoutedEventArgs e)
{
_scrollViewer = FindScrollViewer(this);
if (_scrollViewer != null)
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
CalcPageSize();
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(Reset));
}
private void Host_Unloaded(object sender, RoutedEventArgs e)
{
if (_scrollViewer != null)
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
private void Host_SizeChanged(object sender, SizeChangedEventArgs e)
{
CalcPageSize();
Reset();
}
private void Generator_StatusChanged(object sender, EventArgs e)
{
var gen = sender as ItemContainerGenerator;
if (gen == null || gen.Status != GeneratorStatus.ContainersGenerated)
return;
if (_lazyCtx == null || _lazyCtx.MeasuredItemHeight > 0)
return;
// 自动测量首项高度
var firstItem = gen.ContainerFromIndex(0) as ListBoxItem;
if (firstItem == null) return;
double itemHeight = firstItem.ActualHeight;
if (itemHeight > 0)
{
_lazyCtx.MeasuredItemHeight = itemHeight;
CalcPageSize();
Reset();
}
}
#endregion
#region 动态计算分页大小
private void CalcPageSize()
{
double preload = PreloadScreen;
double itemHeight = _lazyCtx.MeasuredItemHeight > 0 ? _lazyCtx.MeasuredItemHeight : 30;
if (_scrollViewer == null || _scrollViewer.ViewportHeight <= 0)
{
_lazyCtx.PageSize = 20;
return;
}
// 根据视口高度动态计算单页数据量
int oneScreen = (int)Math.Ceiling(_scrollViewer.ViewportHeight / itemHeight);
_lazyCtx.PageSize = (int)Math.Ceiling(oneScreen * preload);
}
#endregion
#region 加载下一页数据
private void LoadNextPage()
{
if (_lazyCtx.IsLoading) return;
IList source = AllItems;
var visible = VisibleItems;
if (source == null || visible == null || _lazyCtx.LoadedItemIndex >= source.Count)
return;
_lazyCtx.IsLoading = true;
try
{
int take = Math.Min(_lazyCtx.PageSize, source.Count – _lazyCtx.LoadedItemIndex);
for (int i = 0; i < take; i++)
{
visible.Add(source[_lazyCtx.LoadedItemIndex + i]);
}
_lazyCtx.LoadedItemIndex += take;
}
finally
{
_lazyCtx.IsLoading = false;
}
}
#endregion
#region 滚动预加载逻辑
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_lazyCtx == null || _lazyCtx.IsLoading) return;
CalcPageSize();
IList source = AllItems;
if (source == null || _lazyCtx.LoadedItemIndex >= source.Count) return;
double itemHeight = _lazyCtx.MeasuredItemHeight > 0 ? _lazyCtx.MeasuredItemHeight : 30;
double remain = _scrollViewer.ExtentHeight – (_scrollViewer.VerticalOffset + _scrollViewer.ViewportHeight);
// 距离底部小于半行高度,触发预加载
if (remain < itemHeight * 0.5)
{
LoadNextPage();
}
}
#endregion
#region 对外公开重置方法
public void Reset()
{
if (_lazyCtx == null || VisibleItems == null) return;
IList source = AllItems;
if (source == null) return;
VisibleItems.Clear();
_lazyCtx.LoadedItemIndex = 0;
_lazyCtx.IsLoading = false;
LoadNextPage();
}
#endregion
}
}
七、XAML 使用示例
7.1 LazyDataGrid 表格用法
<local:LazyDataGrid AutoGenerateColumns="False"
IsEnabled="True"
PreloadScreen="1.2"
AllItems="{Binding Items}">
<local:LazyDataGrid.Columns>
<DataGridTextColumn Header="编号" Binding="{Binding Id}"/>
<DataGridTextColumn Header="数据名称" Binding="{Binding Name}"/>
</local:LazyDataGrid.Columns>
</local:LazyDataGrid>
7.2 LazyListBox 列表用法
<local:LazyListBox IsEnabled="True"
PreloadScreen="1.2"
AllItems="{Binding Items}">
<local:LazyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="6,4" FontSize="14"/>
</DataTemplate>
</local:LazyListBox.ItemTemplate>
</local:LazyListBox>
八、窗口测试后台代码
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
namespace WpfAttachedLazyLoad
{
public partial class MainWindow : Window
{
public LazyListViewModel Vm { get; set; } = new LazyListViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = Vm;
// 初始化十万条测试大数据
LoadFirstBigData();
}
/// <summary>
/// 首次加载十万条测试数据
/// </summary>
private void LoadFirstBigData()
{
var list = new List<ItemModel>();
for (int i = 1; i <= 100000; i++)
{
list.Add(new ItemModel { Id = i, Name = $"测试数据_{i}" });
}
Vm.Items = new ObservableCollection<ItemModel>(list);
}
/// <summary>
/// 动态刷新替换数据源(控件自动重置,无需手动编码)
/// </summary>
private void Btn_Refresh_Click(object sender, RoutedEventArgs e)
{
var newList = new List<ItemModel>();
for (int i = 2000; i <= 80000; i++)
{
newList.Add(new ItemModel { Id = i, Name = $"刷新后数据_{i}" });
}
Vm.Items = new ObservableCollection<ItemModel>(newList);
}
}
}
九、关键技术总结 & 避坑清单
1. 彻底放弃附加属性的原因
附加属性绑定在替换数据源集合场景下存在原生通知丢失问题,属于WPF底层固有缺陷,无法通过业务代码修复,会导致二次刷新界面无响应。
2. 彻底放弃Behavior的原因
Behavior属于Freezable非UI对象,不在可视化树中,依赖属性的更新机制与原生控件不一致,数据源二次替换必然刷新失效,稳定性无法保障。
3. 自定义继承控件方案的核心优势
直接继承原生DataGrid、ListBox实现的自定义控件,完全遵循WPF标准绑定机制,属性变更、数据源替换、多轮刷新均稳定触发回调,彻底根治绑定失效问题。
4. 控件能力边界说明
当前版本为单向向下懒加载(滚动到底部续页),可满足90%以上桌面业务大数据列表场景。如需双向虚拟滚动(上下滚动按需加载区间数据),可基于现有架构二次扩展。
十、结语
这套 LazyDataGrid / LazyListBox 是经过多轮踩坑、架构迭代后的工业级稳定方案:
-
彻底解决 WPF 大数据列表渲染卡顿、UI卡死问题
-
彻底解决行业通用的控件二次刷新不更新的绑定玄学问题
-
纯视图层优化,零侵入ViewModel,贴合MVVM架构
-
无第三方依赖、无内存泄漏,可直接复用至所有WPF项目
后续项目中所有大数据表格、大数据列表场景,可统一替换为本套懒加载控件,实现项目全局UI性能标准化优化。
网硕互联帮助中心





评论前必须登录!
注册