如何异步实现预填充ObservableCollection后ItemsSource正常绑定显示
问题:异步过滤数据后导航并绑定CarouselView的异常问题
我有一个CarouselView,它的ItemsSource绑定了一个ObservableCollection。目前在OnAppearing事件里执行ViewModel的命令时,集合能正常绑定显示。现在想实现:点击第一页的按钮后,先异步过滤数据并填充ObservableCollection,完成后再导航到第二页,但尝试这个方案时出现空引用异常,而且ItemsSource不显示数据。之前试过把ObservableCollection作为参数传到第二页,但复制集合会导致界面卡顿,请问该怎么正确实现这个异步处理逻辑?
可用代码(当前能运行但不符合需求的实现)
第二页代码
public partial class SecondPage : ContentPage { // 构造函数接收筛选参数 public SecondPage(bool hard, string subject) { InitializeComponent(); var vm = (DataSelectionViewModel)BindingContext; vm.Hard = hard; vm.Subject = subject; /* 必须在这里重新设置hard和subject,否则数据加载不出来 */ } protected override async void OnAppearing() { var vm = (DataSelectionViewModel)BindingContext; base.OnAppearing(); // 在页面显示时执行数据加载命令 await vm.LoadData.ExecuteAsync().ConfigureAwait(false); } }
第二页ViewModel
public class DataSelectionViewModel : BaseViewModel { private string _subject; public string Subject { get => _subject; set => SetProperty(ref _subject, value); } private bool _hard; public bool Hard { get => _hard; set => SetProperty(ref _hard, value); } public ObservableCollection<Items> FilteredData { get; set; } private readonly IDataStore _dataStore; public AsyncCommand LoadData { get; } public DataSelectionViewModel() { _dataStore = DependencyService.Get<IDataStore>(); LoadData = new AsyncAwaitBestPractices.MVVM.AsyncCommand(FilterData); // 初始化集合 FilteredData = new ObservableCollection<Items>(); } // 异步筛选数据 public async Task FilterData() { FilteredData.Clear(); var filteredData = await _dataStore.SearchData(Hard, Subject).ConfigureAwait(false); foreach (var data in filteredData) { FilteredData.Add(data); } } }
第一页按钮点击事件
private async void ButtonClick(object sender, EventArgs e) { var vm = (BaseViewModel)BindingContext; vm.Hard = HardButtonSelected == Hard; vm.Subject = vm.Subject.ToLower(); // 直接导航到第二页,数据在第二页的OnAppearing中加载 await Navigation.PushAsync(new SecondPage(vm.Hard, vm.Subject)); }
不可用代码(尝试实现需求但出错的版本)
第二页代码
public partial class SecondPage : ContentPage { public SecondPage() { InitializeComponent(); } }
第二页ViewModel
public class DataSelectionViewModel : BaseViewModel { public ObservableCollection<Items> FilteredData { get; set; } public DataSelectionViewModel() { FilteredData = new ObservableCollection<Items>(); } }
BaseViewModel
public class BaseViewModel : INotifyPropertyChanged { private string _subject; public string Subject { get => _subject; set => SetProperty(ref _subject, value); } private bool _hard; public bool Hard { get => _hard; set => SetProperty(ref _hard, value); } public ObservableCollection<Items> FilteredData { get; set; } /* BaseViewModel中实现了SetProperty方法 */ }
第一页按钮点击事件
private async void ButtonClick(object sender, EventArgs e) { var vm = (BaseViewModel)BindingContext; vm.Hard = HardButtonSelected == Hard; vm.Subject = vm.Subject.ToLower(); }
第一页ViewModel
public class FirstPageViewModel : BaseViewModel { public IAsyncCommand ButtonClickedCommand { get; } readonly IPageService _pageService; readonly IDataStore _dataStore; public FirstPageViewModel() { _dataStore = DependencyService.Get<IDataStore>(); ButtonClickedCommand = new AsyncCommand(ButtonClicked); _pageService = DependencyService.Get<IPageService>(); } private async Task ButtonClicked() { // 先筛选数据 await FilterData(); // 再导航到第二页 await _pageService.PushAsync(new SecondPage()); } private async Task FilterData() { FilteredData.Clear(); var datas = await _dataStore.SearchData(Subject, Hard).ConfigureAwait(false); foreach (var data in datas) { FilteredData.Add(data); } } }
解决方法
1. 修复空引用异常
空异常的核心原因是FirstPageViewModel中的FilteredData未初始化就调用了Clear(),在构造函数中补充初始化即可:
public FirstPageViewModel() { _dataStore = DependencyService.Get<IDataStore>(); ButtonClickedCommand = new AsyncCommand(ButtonClicked); _pageService = DependencyService.Get<IPageService>(); // 初始化集合,避免空引用 FilteredData = new ObservableCollection<Items>(); }
2. 实现“筛选完成后再导航”的正确逻辑
方案A:共享ViewModel实例(推荐)
让第二页直接复用第一页的ViewModel,完全避免集合复制操作:
- 修改第二页构造函数,接收ViewModel实例:
public partial class SecondPage : ContentPage { public SecondPage(BaseViewModel vm) { InitializeComponent(); // 直接绑定第一页的ViewModel BindingContext = vm; } }
- 修改第一页ViewModel的导航逻辑:
private async Task ButtonClicked() { await FilterData(); // 传递当前ViewModel给第二页 await _pageService.PushAsync(new SecondPage(this)); }
方案B:批量传递数据优化性能
如果必须使用独立ViewModel,不要复制集合,而是传递筛选后的原始列表并批量添加,减少UI更新次数:
- 给第二页ViewModel添加批量加载数据的方法:
public class DataSelectionViewModel : BaseViewModel { public ObservableCollection<Items> FilteredData { get; set; } public DataSelectionViewModel() { FilteredData = new ObservableCollection<Items>(); } // 批量添加数据,减少UI线程更新次数 public void LoadFilteredData(List<Items> data) { FilteredData.Clear(); foreach(var item in data) { FilteredData.Add(item); } } }
- 修改第一页导航逻辑:
private async Task ButtonClicked() { var datas = await _dataStore.SearchData(Subject, Hard).ConfigureAwait(false); var secondVm = new DataSelectionViewModel(); secondVm.LoadFilteredData(datas.ToList()); await _pageService.PushAsync(new SecondPage { BindingContext = secondVm }); }
3. 避免UI线程阻塞
ObservableCollection的操作必须在UI线程执行,若筛选逻辑使用了ConfigureAwait(false),需切换回UI线程更新集合:
private async Task FilterData() { FilteredData.Clear(); var datas = await _dataStore.SearchData(Subject, Hard).ConfigureAwait(false); // 切换回UI线程更新集合 MainThread.BeginInvokeOnMainThread(() => { foreach (var data in datas) { FilteredData.Add(data); } }); }
内容的提问来源于stack exchange,提问作者ViDanMaster
相关产品推荐
相关产品推荐

