如何在.NET MAUI异步方法中使用foreach避免UI卡顿?
我正在开发一款.NET MAUI Android应用,现有一个方法从数据库获取数据后,通过foreach循环将数据添加到ObservableCollection中。但数据量较大时,这个过程耗时久,会导致UI卡顿冻结,想找一种不阻塞UI的优化方案。
原参考代码
ViewModel代码
private IDataService _dataService; public ObservableCollection<Item> Items { get; set; } public MainPageViewModel(IDataService dataService) { Items = new ObservableCollection<Item>(); _dataService = dataService; } public async Task LoadItemsFromDbAsync() { List<Item> ItemList = await _dataService.GetItemsAsync(); foreach(Item item in ItemList) { Items.Add(item); } }
MainPage.xaml.cs代码
private MainPageViewModel _viewModel; public MainPage(MainPageViewModel viewModel) { InitializeComponent(); _viewModel = viewModel; BindingContext = _viewModel; } protected async override void OnNavigatedTo(NavigatedToEventArgs args) { base.OnNavigatedTo(args); await _viewModel.LoadItemsFromDbAsync(); }
已尝试的无效方案
方案1(报错)
执行时报错:Android.Util.AndroidRuntimeException:** 'Only the original thread that created a view hierarchy can touch its views.
public ObservableCollection<Item> Items { get; set; } public async Task LoadItemsFromDbAsync() { List<Item> ItemList = await _dataService.GetItemsAsync(); var tasks = new List<Task>(); foreach (Item item in ItemList) { tasks.Add(Task.Run(() => AddItemToCollection(item))); } await Task.WhenAll(tasks); } private void AddItemToCollection(Item item) { Items.Add(item); }
原因:ObservableCollection属于UI绑定集合,必须在UI线程操作,Task.Run把操作放到了后台线程,违反了线程规则。
方案2(仍卡顿)
能运行但效果和原代码一致,UI依旧阻塞到全部添加完成:
public ObservableCollection<Item> Items { get; set; } public async Task LoadItemsFromDbAsync() { List<Item> ItemList = await _dataService.GetItemsAsync(); var tasks = ItemList.Select(async item => { Items.Add(item); }); await Task.WhenAll(tasks); }
原因:Select里的async没有实际异步操作,所有Add还是在UI线程同步执行,和原foreach没区别。
可行优化方案
方案一:分批添加+让出UI线程
把数据分成小批次,每添加一批就短暂等待,让UI线程有时间处理渲染,避免卡顿:
public async Task LoadItemsFromDbAsync() { List<Item> ItemList = await _dataService.GetItemsAsync(); int batchSize = 50; // 根据实际情况调整批次大小 for (int i = 0; i < ItemList.Count; i += batchSize) { var batch = ItemList.Skip(i).Take(batchSize); foreach (var item in batch) { Items.Add(item); } // 让出UI线程处理渲染 await Task.Delay(1); } }
方案二:扩展ObservableCollection实现AddRange
ObservableCollection原生没有AddRange方法,自己扩展后可以一次性添加所有数据,只触发一次CollectionChanged事件,大幅减少UI更新次数:
// 扩展方法类 public static class ObservableCollectionExtensions { public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items) { foreach (var item in items) { collection.Add(item); } } } // ViewModel中使用 public async Task LoadItemsFromDbAsync() { List<Item> ItemList = await _dataService.GetItemsAsync(); // 先清空原有数据(如果需要) Items.Clear(); // 一次性添加所有数据 Items.AddRange(ItemList); }
注:如果使用CommunityToolkit.Mvvm库,里面已经内置了ObservableCollectionExtensions.AddRange方法,可直接调用。
方案三:开启CollectionView虚拟化
如果页面用的是CollectionView展示数据,开启虚拟化后,UI只会渲染当前可见区域的项,即使数据量很大也不会卡顿:
<CollectionView ItemsSource="{Binding Items}" IsVirtualizing="True" VirtualizationMode="Recycling"> <!-- 你的ItemTemplate --> </CollectionView>
内容的提问来源于stack exchange,提问作者Erikbusker

