Search code examples
c#xamarinxamarin.formsmvvmpicker

How to populate picker options from async SQLite datasource in MVVM


I need to have a picker (or some other control) offering the user options inside a MVVM implementation (based on Xamarin.Forms MVVM: How to Work with SQLite DB(C# — Xaml)) but cant see how to load the async options into the binding.

I have a detail edit screen for a record type called viewings, with straight forward entry fields bound to a ViewingDetailViewingModel. ...BindingContext = new ViewingDetailViewModel(viewModel ?? new ViewingViewModel(), viewingStore, pageService, viewingsRestStore); in the XAML's codebehind.

In addition to the relatively straight forward entry cells binding to properties of the Viewing's model (forgive me if I don't use the correct nomenclature, new to this), I want to have one of the fields use a picker to save a Client's id based on the user selecting a client.

So I'm guessing, as the SQLite clientstore's get return is async, I should have a command that is triggered from somewhere, I imagine ideally from the codebehind's onAppearing method that updates an IList of Clients that the picker binds to - though I encounter two problems here:

  1. I can't figure out how to trigger the command from there so I'm trying to execute it from the end of the viewmodel's constructor for now.
  2. Doing that (although I don't think because of that), the app crashes once it tries to add the first client to the list while looping through the get results with "...System.NullReferenceException: Object reference not set to an instance of an object at AgentApp.ViewingDetailViewModel.LoadClients (System.Collections.Generic.IList`1[T] Clients)..."

so in my XAML I have

<EntryCell Label="Agt_description" Text="{Binding Path=Viewing.Agt_description}" />
                <ViewCell >
                    <Picker Title="Select a client" ItemsSource="{Binding Clients}"  ItemDisplayBinding ="{Binding Agt_FirstName}" />
                </ViewCell>

In the view's code-behind:

 public ViewingDetailPage(ViewingViewModel viewModel)
        {
            Console.WriteLine("ViewingDetailPage()");
            InitializeComponent();
            var viewingStore = new SQLiteViewingStore(DependencyService.Get<ISQLiteDb>());
            var viewingsRestStore = new RESTViewingStore();
            var pageService = new PageService();
            bool isNewViewing = (viewModel.Agt_name == " ");
            Title = (isNewViewing) ? "New Viewing" : "Edit Viewing";
            if (isNewViewing)
            {
                var app = Application.Current as App;
                viewModel.Agt_ac = app.AgencyIdInt;
                viewModel.Agt_b = app.BranchIDInt;
                viewModel.Agt_at = app.AccountIdInt;
            }
            BindingContext = new ViewingDetailViewModel(viewModel ?? new ViewingViewModel(), viewingStore, pageService, viewingsRestStore);
        }

and then the ViewingDetailViewModel:

class ViewingDetailViewModel : BaseViewModel
    {
        private readonly IViewingStore _viewingStore;
        private readonly IViewingStore _viewingRestStore;
        private readonly IPageService _pageService;
        public Viewing Viewing { get; private set; }
        public ICommand SaveCommand { get; private set; }

        //trying
        public ICommand LoadClientsCommand { get; private set; }
        private IClientStore _clientStore;
        private IList<ClientViewModel> _clients;
        public IList<ClientViewModel> Clients
        {
            get { return _clients; }
            set { SetProperty(ref _clients, value); }   // from BaseViewModel, implements OnPropertyChanged()
        }
        //

        public ViewingDetailViewModel(ViewingViewModel viewModel, IViewingStore viewingStore, IPageService pageService, IViewingStore viewingRestStore)
        {
                        var app = Application.Current as App;
            if (viewModel == null)
                throw new ArgumentNullException(nameof(viewModel));

            _pageService = pageService;
            _viewingStore = viewingStore;
            _viewingRestStore = viewingRestStore;

            LoadClientsCommand = new Command(async () => await LoadClients());

            SaveCommand = new Command(async () =>
            {
                                var SaveDBTask = SaveDB("primary");
                var SaveRESTTask = SaveRest();
                                var saveTasks = new List<Task> { SaveDBTask, SaveRESTTask };
                while (saveTasks.Count > 0)
                {
                    Task finishedTask = await Task.WhenAny(saveTasks);
                    if (finishedTask == SaveDBTask)
                    {
                        Console.WriteLine("SaveDBTask finished:" + SaveDBTask);
                    }
                    else if (finishedTask == SaveRESTTask)
                    {
                        Console.WriteLine("SaveRESTTask finished, do db update:" + SaveRESTTask);
                        await SaveDB("secondary update");
                    }
                    saveTasks.Remove(finishedTask);
                }
                Console.WriteLine("Save tasks complete.");
            });

            Viewing = new Viewing
            {
                Id = viewModel.Id,
                Agt_description = viewModel.Agt_description,
                Agt_name = viewModel.Agt_name,
                RemoteId = viewModel.RemoteId,
                Agt_ac = viewModel.Agt_ac > 0 ? viewModel.Agt_ac : app.AgencyIdInt,
                Agt_b = viewModel.Agt_b > 0 ? viewModel.Agt_b : app.BranchIDInt,
                Agt_at = viewModel.Agt_at > 0 ? viewModel.Agt_at : app.AccountIdInt,
                Agt_pr = viewModel.Agt_pr,
                Agt_datetime_scheduled = viewModel.Agt_datetime_scheduled,
                Agt_datetime_start = viewModel.Agt_datetime_start,
                Agt_datetime_end = viewModel.Agt_datetime_end,
                StatusLocal = viewModel.StatusLocal,
                CreatedLocal = viewModel.CreatedLocal,
                ModifiedLocal = viewModel.ModifiedLocal,
                CreatedRemote = viewModel.CreatedRemote,
                ModifiedRemote = viewModel.ModifiedRemote,
                LastSync = viewModel.LastSync,
            };

            LoadClientsCommand.Execute(null);//doubt this is a good idea
        }

        async Task LoadClients()
        {
            _clientStore = new SQLiteClientStore(DependencyService.Get<ISQLiteDb>());
            var clients = await _clientStore.GetClientsAsync();
            foreach (var client in clients)
            {
                Console.WriteLine("for client:" + client.Agt_FirstName);
                Clients.Add(new ClientViewModel(client)); //crashes around here
            }
        }

        async Task SaveDB(string savetype)
        {
            if (String.IsNullOrWhiteSpace(Viewing.Agt_name))
            {
                await _pageService.DisplayAlert("Error", "Please enter the name.", "OK");
                return;
            }
            if (Viewing.Id == 0)
            {
                await _viewingStore.AddViewing(Viewing);
                MessagingCenter.Send(this, Events.ViewingAdded, Viewing);
            }
            else
            {
                await _viewingStore.UpdateViewing(Viewing);
                MessagingCenter.Send(this, Events.ViewingUpdated, Viewing);
            }
            if (savetype == "primary")
                await _pageService.PopAsync();
        }

        async Task SaveRest()
        {
            if (String.IsNullOrWhiteSpace(Viewing.Agt_name))
            {
                Console.WriteLine("REST save could not, invalid name.");
                return;
            }
            if (Viewing.RemoteId == 0)
            {
                await _viewingRestStore.AddViewing(Viewing);
            }
            else
            {
                await _viewingRestStore.UpdateViewing(Viewing);
                MessagingCenter.Send(this, Events.ViewingUpdated, Viewing);
            }
        }
    }

I'm a complete newb coming from a simple php background and all this OOP complexity is confusing me somewhat; I'm probably making a whole lot of really stupid mistakes - and maybe I'm overcomplicating things - I hope someone can point me at a good simple way of loading the picker's async data.

Thanks in advance!


Solution

  • First, it doesn't look like you are instantiating the Clients property anywhere. That is why you would get a NullReferenceException

    Second, if you are adding items to a IList no one will get notified that something was added to this collection. Instead, you should consider changing the type to ObservableCollection instead, that helps you doing so.

    So changing the code for Clients to:

    public ObservableCollection<ClientViewModel> Clients { get; }
        = new ObservableCollection<ClientViewModel>();
    

    This will probably solve most of your issues with UI not updating and Clients being null when you try populating it.