如何在MAUI应用中复用全局已加载的Observable Collection
我开发了一个MAUI应用,其中有4-5个Observable Collection在不同视图中使用。目前通过ToDoService从数据库加载数据,触发事件更新Observable Collection,但每个使用数据的页面都需要重新加载数据。
ToDoService 代码
public class ToDoService { private readonly IUnitOfWork _unitOfWork; private readonly List<ToDo> _toDos; public IEnumerable<ToDo> ToDos => _toDos; public event Action ToDosLoaded; public event Action<ToDo> ToDoAdded; public event Action<ToDo> ToDoUpdated; public event Action<Guid> ToDoDeleted; public ToDoService(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; _toDos = new List<ToDo>(); } public async Task Load() { IEnumerable<ToDo> toDos = await _unitOfWork.ToDo.GetAllAsync(); _toDos.Clear(); _toDos.AddRange(toDos); ToDosLoaded?.Invoke(); } public async Task Add(ToDo toDo, bool isCopy = false) { var saved = await _unitOfWork.ToDo.SaveAsync(toDo); if (saved is not null) { _toDos.Add(toDo); ToDoAdded?.Invoke(toDo); } } public async Task Update(ToDo toDo) { var saved = await _unitOfWork.ToDo.SaveAsync(toDo); if (saved is not null) { int currentIndex = _toDos.FindIndex(y => y.LocalId == saved.LocalId); if (currentIndex != -1) { _toDos[currentIndex] = saved; } else { _toDos.Add(saved); } ToDoUpdated?.Invoke(saved); } } public async Task Delete(ToDo toDo) { var deleted = await _unitOfWork.ToDO.DeleteAsync(toDo.LocalId); if (deleted) { _toDos.RemoveAll(g => g.LocalId == toDo.LocalId); ToDoDeleted?.Invoke(toDo.LocalId); } } }
ToDoListingViewModel 代码
private readonly ToDoService _toDoStoreService; [ObservableProperty] private ObservableCollection<ToDoListingPreviewItemViewModel> _toDoListingPreviewItemViewModels; public ToDoListingViewModel(ToDoService toDoStoreService) { ToDoListingPreviewItemViewModels = []; _toDoStoreService = toDoStoreService; _toDoStoreService.ToDosLoaded += ToDoStoreService_ToDosLoaded; _toDoStoreService.ToDoAdded += ToDoStoreService_ToDoAdded; _toDoStoreService.ToDoUpdated += ToDoStoreService_ToDoUpdated; _toDoStoreService.ToDoDeleted += ToDoStoreService_ToDoDeleted; ToDoListingPreviewItemViewModels.CollectionChanged += ToDoListingItemViewModels_CollectionChanged; } private void ToDoStoreService_ToDosLoaded() { ToDoListingPreviewItemViewModels.Clear(); foreach (ToDo toDo in _toDoStoreService.ToDos) { AddToDo(toDo); } } private void ToDoStoreService_ToDoAdded(ToDo toDo) { AddToDo(toDo); } private void ToDoStoreService_ToDoUpdated(ToDo toDo) { ToDoListingPreviewItemViewModel toDoListingPreviewItemViewModel = ToDoListingPreviewItemViewModels.FirstOrDefault(t => t.ToDo.LocalId == toDo.LocalId); if (toDoListingPreviewItemViewModel != null) { toDoListingPreviewItemViewModel.Update(toDo); } } private void ToDoStoreService_ToDoDeleted(Guid id) { ToDoListingPreviewItemViewModel toDoListingPreviewItemViewModel = ToDoListingPreviewItemViewModels.FirstOrDefault(y => y.ToDo?.LocalId == id); if (toDoListingPreviewItemViewModel != null) { ToDoListingPreviewItemViewModels.Remove(toDoListingPreviewItemViewModel); } } private void ToDoListingItemViewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(Name)); } private void AddToDo(ToDo toDo) { ToDoListingPreviewItemViewModel toDoListingPreviewItemViewModel = new(toDo); ToDoListingPreviewItemViewModels.Add(toDoListingPreviewItemViewModel); } public void Dispose() { _toDoStoreService.ToDosLoaded -= ToDoStoreService_ToDosLoaded; _toDoStoreService.ToDoAdded -= ToDoStoreService_ToDoAdded; _toDoStoreService.ToDoUpdated -= ToDoStoreService_ToDoUpdated; _toDoStoreService.ToDoDeleted -= ToDoStoreService_ToDoDeleted; ToDoListingPreviewItemViewModels.CollectionChanged -= ToDoListingItemViewModels_CollectionChanged; }
当前使用方式
public ToDosListingViewModel ToDos { get; } ToDos = new ToDosListingViewModel (_todosStoreService); // 加载数据 await _todosStoreService.Load();
问题
当前设置下,任何页面使用该集合都需先加载数据,数据变更会同步更新所有已加载的列表。虽然能保证一致性,但我希望避免重复加载数据和重复订阅事件,如何复用单一集合实例?
是否应将ToDosListingViewModel和ToDoService设为单例,在应用启动时一次性加载所有数据?或是在App.cs中添加全局列表变量?
优化方案
1. 将ToDoService注册为单例(优先推荐)
MAUI依赖注入系统支持单例注册,确保整个应用只有一个ToDoService实例,内部的_toDos列表唯一,彻底避免重复加载。
- 注册步骤:在
MauiProgram.cs的CreateMauiApp方法中添加:
builder.Services.AddSingleton<ToDoService>();
- 修改Load方法:增加加载状态标记,防止重复调用:
private bool _isLoaded = false; public async Task Load() { if (_isLoaded) return; IEnumerable<ToDo> toDos = await _unitOfWork.ToDo.GetAllAsync(); _toDos.Clear(); _toDos.AddRange(toDos); _isLoaded = true; ToDosLoaded?.Invoke(); }
这样无论哪个ViewModel调用Load,数据只会加载一次。
2. 让ToDoService直接暴露ObservableCollection
把内部的_toDos替换为ObservableCollection<ToDo>,利用其原生的变更通知机制,无需手动触发事件,ViewModel可直接绑定或同步这个全局集合:
public class ToDoService { private readonly IUnitOfWork _unitOfWork; public ObservableCollection<ToDo> ToDos { get; } = new(); public async Task Load() { if (ToDos.Any()) return; // 避免重复加载 var toDos = await _unitOfWork.ToDo.GetAllAsync(); foreach (var todo in toDos) { ToDos.Add(todo); } } public async Task Add(ToDo toDo, bool isCopy = false) { var saved = await _unitOfWork.ToDo.SaveAsync(toDo); if (saved != null) { ToDos.Add(saved); } } public async Task Update(ToDo toDo) { var saved = await _unitOfWork.ToDo.SaveAsync(toDo); if (saved != null) { var existing = ToDos.FirstOrDefault(t => t.LocalId == saved.LocalId); if (existing != null) { var index = ToDos.IndexOf(existing); ToDos[index] = saved; } else { ToDos.Add(saved); } } } public async Task Delete(ToDo toDo) { var deleted = await _unitOfWork.ToDO.DeleteAsync(toDo.LocalId); if (deleted) { var existing = ToDos.FirstOrDefault(t => t.LocalId == toDo.LocalId); if (existing != null) ToDos.Remove(existing); } } }
ViewModel侧只需监听全局集合的变更,同步自己的预览模型集合:
public class ToDoListingViewModel { public ObservableCollection<ToDoListingPreviewItemViewModel> PreviewItems { get; } public ToDoListingViewModel(ToDoService toDoService) { PreviewItems = new ObservableCollection<ToDoListingPreviewItemViewModel>( toDoService.ToDos.Select(t => new ToDoListingPreviewItemViewModel(t)) ); // 同步全局集合的变更 toDoService.ToDos.CollectionChanged += (s, e) => { switch (e.Action) { case NotifyCollectionChangedAction.Add: foreach (ToDo item in e.NewItems!) { PreviewItems.Add(new ToDoListingPreviewItemViewModel(item)); } break; case NotifyCollectionChangedAction.Remove: foreach (ToDo item in e.OldItems!) { var preview = PreviewItems.FirstOrDefault(p => p.ToDo.LocalId == item.LocalId); if (preview != null) PreviewItems.Remove(preview); } break; case NotifyCollectionChangedAction.Replace: foreach (ToDo item in e.NewItems!) { var preview = PreviewItems.FirstOrDefault(p => p.ToDo.LocalId == item.LocalId); if (preview != null) preview.Update(item); } break; } }; } }
这种方式减少了手动事件订阅的冗余代码,利用MAUI原生的集合通知机制。
3. ViewModel不建议设为单例
除非是全局持久化的视图(如底部导航栏的固定页面),否则不要将ViewModel注册为单例。MAUI页面通常在导航时创建,单例ViewModel会导致状态残留,增加内存泄漏风险。正确的做法是每个ViewModel通过构造函数注入单例ToDoService,独立实例但共享同一数据源。
4. 不推荐使用App.cs全局变量
全局变量会破坏依赖注入的解耦性,降低代码可维护性和可测试性,后续扩展和调试都会变得麻烦,因此不建议采用。
总结
最优实现路径:
- 将ToDoService注册为单例,确保数据唯一且仅加载一次;
- ToDoService内部使用ObservableCollection,利用原生变更通知;
- ViewModel注入单例ToDoService,按需转换模型并同步数据。
内容的提问来源于stack exchange,提问作者Zoltán

