mvvmmauiobservablecollection

How to reuse globally already loaded observable collection in Maui app?


I have a Maui app where 4-5 observable collections are used across the application on different views. Currently I have a service which loads from database via repository and trigger an event to update the observable collection but this needs to be loaded every single pages where this data is used.

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;
}

With this way I can simply create

public ToDosListingViewModel ToDos { get; }
ToDos = new ToDosListingViewModel (_todosStoreService);
// and load data
await _todosStoreService.Load();

With this setup if any page needs to use this collection I need to load all first and if anywhere a change happens it will trigger all lists which have been loaded before to update all of them across the application. It is great but I would like to avoid to load multiple times and subscribing for the events to update. How can I use one list property across the application?

Shall I just make ToDosListingViewModel and ToDoService singleton and when the app starts i just load all data once and thats all? Shall I add a global list variable to the App.cs?


Solution

  • One way to do this is to create and register a GlobalViewModel. Note that it is enough to declare ObservableCollection with a getter and use an initializer or constructor to initialize it. There's no need to use the [ObservableProperty] attribute.

    public class GlobalViewModel
    {
         public ObservableCollection<ToDoListingPreviewItemViewModel> ToDoListingPreviewItemViewModels {get;} = new;
    }
    

    Then, you register this GlobalViewModal as a singleton:

        // MauiProgram.cs
        builder.Services.AddSingleton<GlobalViewModel>();
        builder.Services.AddTransient<MainPage>();
    

    Then, for all pages that need access to it, you can use dependency injection and gain a copy of it:

    // MainPage.xaml.cs
    public class MainPage
    {
        public GlobalViewModel GVM { get;}
    
        public MainPage(GlobalViewModel gvm)
        {
            this.GVM = gvm;
            InitializeComponent();
            BindingContext = this;
        }
    }
    

    And, in your XAML, you can access the collection with GVM.ToDoListingPreviewItemViewModels