You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在MAUI应用中复用全局已加载的Observable Collection

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全局变量

全局变量会破坏依赖注入的解耦性,降低代码可维护性和可测试性,后续扩展和调试都会变得麻烦,因此不建议采用。

总结

最优实现路径:

  1. 将ToDoService注册为单例,确保数据唯一且仅加载一次;
  2. ToDoService内部使用ObservableCollection,利用原生变更通知;
  3. ViewModel注入单例ToDoService,按需转换模型并同步数据。

内容的提问来源于stack exchange,提问作者Zoltán

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 16:05:53