WPF系统一加载超过1万条数据就卡死,用户体验极差,老板天天催优化。这种场景相信很多C#开发者都遇到过:数据量一大,ListView就成了"性能杀手"。其实这个问题在Winform中一样,解决方案也是类似。
实际测试未优化的ListView在加载5000+条数据时,渲染时间超过3秒,内存占用直线飙升。而经过分页优化后,同样的数据量,渲染时间降到200ms以内,内存占用减少80%!
今天这篇文章,我将带你彻底解决WPF ListView大数据加载卡顿的问题,让你的应用真正做到"丝滑流畅"。
C#using 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;
using System.Windows.Input;
using System.Windows;
namespace AppListviewPage
{
// 数据模型
public class UserInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreateTime { get; set; }
}
// 分页参数类
public class PageParameter
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 50;
public int TotalCount { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
}
// ViewModel实现
public class MainViewModel : INotifyPropertyChanged
{
private ObservableCollection<UserInfo> _userList;
private PageParameter _pageParam;
private bool _isLoading;
public ObservableCollection<UserInfo> UserList
{
get => _userList;
set
{
_userList = value;
OnPropertyChanged();
}
}
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
}
}
public ICommand LoadMoreCommand { get; }
public MainViewModel()
{
UserList = new ObservableCollection<UserInfo>();
_pageParam = new PageParameter();
LoadMoreCommand = new RelayCommand(LoadMoreData);
// 初始加载第一页
LoadFirstPage();
}
// 🔑 关键方法:加载第一页数据
private async void LoadFirstPage()
{
IsLoading = true;
try
{
var result = await GetUserListAsync(1, _pageParam.PageSize);
_pageParam.TotalCount = result.TotalCount;
_pageParam.PageIndex = 1;
UserList.Clear();
foreach (var user in result.Data)
{
UserList.Add(user);
}
}
catch (Exception ex)
{
// 错误处理
MessageBox.Show($"加载数据失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
// 🔑 关键方法:加载更多数据
private async void LoadMoreData()
{
// 防止重复加载
if (IsLoading || _pageParam.PageIndex >= _pageParam.TotalPages)
return;
IsLoading = true;
try
{
var nextPage = _pageParam.PageIndex + 1;
var result = await GetUserListAsync(nextPage, _pageParam.PageSize);
_pageParam.PageIndex = nextPage;
// 追加数据到现有列表
foreach (var user in result.Data)
{
UserList.Add(user);
}
}
catch (Exception ex)
{
MessageBox.Show($"加载更多数据失败:{ex.Message}");
}
finally
{
IsLoading = false;
}
}
// 模拟数据获取方法(实际项目中替换为真实API调用)
private async Task<PageResult<UserInfo>> GetUserListAsync(int pageIndex, int pageSize)
{
// 模拟网络延迟
await Task.Delay(500);
var totalCount = 50000; // 模拟总数据量
var startIndex = (pageIndex - 1) * pageSize;
var data = new List<UserInfo>();
for (int i = 0; i < pageSize && startIndex + i < totalCount; i++)
{
var index = startIndex + i + 1;
data.Add(new UserInfo
{
Id = index,
Name = $"用户{index:D4}",
Email = $"user{index}@example.com",
CreateTime = DateTime.Now.AddDays(-index)
});
}
return new PageResult<UserInfo>
{
Data = data,
TotalCount = totalCount
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// 分页结果类
public class PageResult<T>
{
public List<T> Data { get; set; }
public int TotalCount { get; set; }
}
}
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace AppListviewPage
{
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
}
}
C#using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows;
namespace AppListviewPage
{
// 布尔值到可见性转换器
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
}
XML<Window x:Class="AppListviewPage.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:AppListviewPage"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<TextBlock Grid.Row="0" Text="用户列表" FontSize="16" FontWeight="Bold"
Margin="10" HorizontalAlignment="Center"/>
<!-- 🔑 关键配置:ListView with 虚拟化 -->
<ListView Grid.Row="1" ItemsSource="{Binding UserList}"
ScrollViewer.ScrollChanged="ListView_ScrollChanged"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" Width="80" DisplayMemberBinding="{Binding Id}"/>
<GridViewColumn Header="姓名" Width="150" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="邮箱" Width="200" DisplayMemberBinding="{Binding Email}"/>
<GridViewColumn Header="创建时间" Width="150"
DisplayMemberBinding="{Binding CreateTime, StringFormat=yyyy-MM-dd}"/>
</GridView>
</ListView.View>
</ListView>
<!-- 加载状态栏 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="10">
<TextBlock Text="数据总数:"/>
<TextBlock Text="{Binding UserList.Count}"/>
<TextBlock Text=" / " Margin="5,0"/>
<!-- 加载指示器 -->
<StackPanel Orientation="Horizontal" Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressBar Width="100" Height="15" IsIndeterminate="True" Margin="10,0"/>
<TextBlock Text="加载中..." VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
C#using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace AppListviewPage
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainViewModel();
DataContext = _viewModel;
}
// 🔑 关键方法:滚动到底部自动加载
private void ListView_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = e.OriginalSource as ScrollViewer;
if (scrollViewer == null) return;
// 检测是否滚动到底部(预留50像素提前加载)
if (scrollViewer.VerticalOffset >= scrollViewer.ScrollableHeight - 50)
{
// 触发加载更多数据
if (_viewModel.LoadMoreCommand.CanExecute(null))
{
_viewModel.LoadMoreCommand.Execute(null);
}
}
}
}
}

通过这套完整的分页加载方案,你可以轻松解决WPF ListView大数据性能问题:
三个"收藏级"代码模板:
记住这个黄金法则:数据量 > 1000条,必须分页;数据量 > 5000条,必须虚拟化!
💬 互动交流:
觉得这套方案有用的话,请转发给更多需要的同行,让我们一起提升WPF应用的用户体验!
关注我,获取更多C#开发实战技巧和最佳实践分享!🚀
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!