云计算百科
云计算领域专业知识百科平台

WPF 高性能懒加载控件 LazyDataGrid / LazyListBox 实现与踩坑全复盘

一、前言: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性能标准化优化。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » WPF 高性能懒加载控件 LazyDataGrid / LazyListBox 实现与踩坑全复盘
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!