Best way to load data from SQLite into a ViewModel

EdiporeiEdiporei Raelson AraújoBRMember ✭✭

Hi!

I've been struggling a little as the best approach for the matter on the title. For some time I was believing that a good way to do this was to download the SQLite database by overriding the OnAppearing method of a page, like this:

`

async protected override void OnAppearing()
    {
        base.OnAppearing();

        lsv_myListView.ItemsSource = await App.Bank.GetItemsAsync();

    }

`

And by having on Xaml a TwoWay binding on the ItemsSource property of the ListView, I could easily populate the ObservableCollection on my ViewModel. Everything works...except that, from what I've noticed on the tests, ItemsSource cannot work as a TwoWay binding, only OneWay. My workaround for this would be to use MessagingCenter, in which everything works properly...but I really dislike going that way. What would you guys recommend as the best approach to populate a ListView and it's ObservableCollection with a SQLite database?

Obs.: I need the Observable Collection on the ViewModel because this data must be modifiable by the user.

Best Answers

  • JulienRosenJulienRosen Julien Rosen CAMember ✭✭✭✭
    Accepted Answer

    ObservableCollections should be declared like so:

    public ObservableCollection<string> ListItems { get; } = new ObservableCollection<string>();

    You should not be raising PropertyChanged on an OC. It would only trigger when you set the OC, aka new it. Adding and removing from the OC is already automatically taken care of, you do not need INPC.

    In your xaml, you are setting the ItemsSource binding.

    <ListView x:Name="lsv_Items" ItemsSource={Binding ItemList, Mode=Twoway}>

    You do not need to specify this as TwoWay. The default mode is what you want.

    In your code, you are resetting the ItemsSource.

    lsv_Items.ItemsSource = await App.Bank.GetItemsAsync();

    You have already bound it in your XAML, you do not need to assign it again. Pro-tip: when you name your UI elements, and start setting properties via name in your code-behind, you are probably breaking MVVM.

    Using the OnAppearing method to trigger a population of your collection is an okay approach.
    However, you should be resolving the view model of the page, and triggering a method on the VM which does the population, since that is where your collection lives already.

                protected override async void OnAppearing()
                {
                    base.OnAppearing();
    
                    var pageVm = this.BindingContext as MyPageVm;
    
                    if (pageVm != null)
                    {
                        pageVm.PopulateCollection();
                    }
                }
    
  • JulienRosenJulienRosen Julien Rosen CAMember ✭✭✭✭
    edited October 12 Accepted Answer

    the problem is that you are creating a new OC. you need to declare your OC once, and then use .Add and .Remove to manipulate it. Do not new it in your PopulateCollection method. If you declare your OC's like I suggest, you can't even do this, because there is no set.

    If you have a List<T>, you iterate through it, and add each item individually to your OC.

  • DirkWilhelmDirkWilhelm Dirk Wilhelm USMember ✭✭✭
    Accepted Answer

    Everytime you are adding an item to an ObservableCollection the CollectionChanged event is triggered and the listview will be redrawn, so if you add a lot of items this can be really bad.

    Take a look at the ObservableRangeCollection in James Montemagnos MvvmHelpers:

    https://github.com/jamesmontemagno/mvvm-helpers

    https://channel9.msdn.com/Shows/XamarinShow/The-Xamarin-Show-12-MVVM-Helpers#time=14m00s

Answers

  • batmacibatmaci Emil A. DEMember ✭✭✭✭

    your question is not clear but as much as I can understand, yes always use ObservableCollection. Mvvm is better approach with Binding instead of directly assigning ItemsSource property.
    you can download your data using api, wcf or any other webservice approach or you can embed your data in sqlite into the project if your db is small and you dont want to amend it frequently. Akavache is good library for downloading from cloud database.

  • EdiporeiEdiporei Raelson Araújo BRMember ✭✭

    Thanks for the reply! I'll do some research about Akavache.

    Let me illustrate a bit more about my situation:

    Here's a sample of my ViewModel

    `

    public class MyViewModel : INotifyPropertyChanged
    {
    
        public MyViewModel()
        {
    
    
        }
    
        public ObservableCollection<ItemModel> _itemlist = new ObservableCollection<ItemModel>();
        public ObservableCollection<ItemModel> ItemList
        {
            get { return _itemlist ; }
            set { _itemlist = value; OnPropertyChanged("ItemList"); }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged(string propertyName)
        {
            var changed = PropertyChanged;
            if (changed != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
    }
    

    `

    At first, I wished to load the data from my SQLite database into my ItemList ObservableCollection by using a TwoWay binding in my listview:

    `

    <ListView x:Name="lsv_Items" ItemsSource={Binding ItemList, Mode=Twoway}>
        <ListView.ItemTemplate>
    
            //Here comes the DataTemplate based on the ItemModel...
    
        </ListView.ItemTemplate>
    </ListView>
    

    `

    And the OnAppearing method of the page would populate the lsv_Items's ItemsSource property, which would send the data to the ObservableCollection inside the ViewModel through it's TwoWay binding.

    `

    async protected override void OnAppearing()
        {
            base.OnAppearing();
    
            lsv_Items.ItemsSource = await App.Bank.GetItemsAsync();
    
     }
    

    `

    Where Bank is a SQLite database.

    But this approach won't work because the TwoWay binding on the Xaml doesn't work for the ItemsSource property (which is a shame, really). So my workaround for this was to populate the ObservableCollection using the Messaging Center class instead, by doing...

    On the constructor of MyViewModel:

    `

    public MetodoViewModel()
        {
            MessagingCenter.Subscribe<ObservableCollection<ItemModel>>(this, "MessageItemList", (sender) =>
            { ItemList = sender; });
        }
    

    `

    And on the OnAppearing method, instead of assigning the ItemsSource directly into the control, I did this:

    `

    async protected override void OnAppearing()
        {
            base.OnAppearing();
    
            var ItemList = new ObservableCollection<ItemModel>(await App.Bank.GetItemsAsync());
                MessagingCenter.Send(ItemList, "MessageItemList");
    
     }
    

    `

    This would send the ItemList directly into the ViewModel, which would populate the ListView through the ItemsSource Binding:

    `

    <ListView x:Name="lsv_Items" ItemsSource={Binding ItemList}>
        <ListView.ItemTemplate>
    
            //Here comes the DataTemplate based on the ItemModel...
    
        </ListView.ItemTemplate>
    </ListView>
    

    `

    But I really dislike this approach because, if I rely too much on the MessagingCenter, it's very time consuming to debug the comings and goings of all these messages since I might lose track of them. So it's a solution I'd like to avoid whenever possible...but in this case I'm being unable to.

    It's my first Xamarin and MVVM project, so I'm also trying to learn as much as possible on the go.

  • DirkWilhelmDirkWilhelm Dirk Wilhelm USMember ✭✭✭

    Why do you want to load your data in the codebehind file of your view? Load it directly in the viewmodel.

  • JulienRosenJulienRosen Julien Rosen CAMember ✭✭✭✭
    Accepted Answer

    ObservableCollections should be declared like so:

    public ObservableCollection<string> ListItems { get; } = new ObservableCollection<string>();

    You should not be raising PropertyChanged on an OC. It would only trigger when you set the OC, aka new it. Adding and removing from the OC is already automatically taken care of, you do not need INPC.

    In your xaml, you are setting the ItemsSource binding.

    <ListView x:Name="lsv_Items" ItemsSource={Binding ItemList, Mode=Twoway}>

    You do not need to specify this as TwoWay. The default mode is what you want.

    In your code, you are resetting the ItemsSource.

    lsv_Items.ItemsSource = await App.Bank.GetItemsAsync();

    You have already bound it in your XAML, you do not need to assign it again. Pro-tip: when you name your UI elements, and start setting properties via name in your code-behind, you are probably breaking MVVM.

    Using the OnAppearing method to trigger a population of your collection is an okay approach.
    However, you should be resolving the view model of the page, and triggering a method on the VM which does the population, since that is where your collection lives already.

                protected override async void OnAppearing()
                {
                    base.OnAppearing();
    
                    var pageVm = this.BindingContext as MyPageVm;
    
                    if (pageVm != null)
                    {
                        pageVm.PopulateCollection();
                    }
                }
    
  • EdiporeiEdiporei Raelson Araújo BRMember ✭✭

    Why do you want to load your data in the codebehind file of your view? Load it directly in the viewmodel.

    That's what I'm trying to figure out how to do it without breaking the MVVM concept. I'm very new to Xamarin and MVVM, certain things are being a struggle to learn...but it's slowly progressing.

    public ObservableCollection ListItems { get; } = new ObservableCollection();

    I can't put it like this because the SQLite only sends me the table as a List, not as a ObservableCollection. So when I load it from the SQLite database, I must convert it to ObservableCollection using a 'new' statement. And if I don't use INPC on my collection, it won't work as intended...unless I'm missing something?

    The App.Bank.GetItemsAsync() which loads the SQLite data into the collection is written like this:

    `

    public Task<List<ItemModel>> GetItemsAsync()
        {
            return Bank.Table<ItemModel>().ToListAsync();
        }
    

    `

    The '.ToListAsync()' only offers me the collection as a List

    lsv_Items.ItemsSource = await App.Bank.GetItemsAsync();

    You have already bound it in your XAML, you do not need to assign it again. Pro-tip: when you name your UI elements, and start setting properties via name in your code-behind, you are probably breaking MVVM.

    All right, tip taken. I'll avoid that from now on. Thanks a lot!

    `

        protected override async void OnAppearing()
            {
                base.OnAppearing();
    
                var pageVm = this.BindingContext as MyPageVm;
    
                if (pageVm != null)
                {
                    pageVm.PopulateCollection();
                }
            }
    

    `

    YES! At first instance, this is what I was looking for. But now I'm facing an odd issue. In one of my pages, this approach works properly...but in another one, it doesn't.

    In the page that works, the PopulateCollection(); is written like this:

    `

    async public void PopulateCollectionA()
        { 
        ItemsListA = new ObservableCollection<ItemModelA>(await App.Bank.GetItemsAAsync()); 
    }
    

    `

    This works as intented. The other one:

    `

    async public void PopulateCollectionB(int ID)
        { 
        ItemsListB = new ObservableCollection<ItemModelB>(await App.Bank.GetItemsBAsync(ID)); 
    }
    

    `

    Where ID is the value of an indexed column of the database.

    Whenever I open the page where this second populated listview lies, it automatically breaks. I'm not sure if the collection is the cause, since that state won't offer me much info about what happened. Also, before breaking, I can see that the collection was properly populated, so it's possible that it's due to another reason.

    Obs.: both pages share the same ViewModel. One of them contains the ListView with ItemModelA data which doesn't break. When the user taps an item, the other page opens with a ListView containing ItemModelB data. This second one breaks, although both all collections are populated in the same manner.

  • JulienRosenJulienRosen Julien Rosen CAMember ✭✭✭✭
    edited October 12 Accepted Answer

    the problem is that you are creating a new OC. you need to declare your OC once, and then use .Add and .Remove to manipulate it. Do not new it in your PopulateCollection method. If you declare your OC's like I suggest, you can't even do this, because there is no set.

    If you have a List<T>, you iterate through it, and add each item individually to your OC.

  • DirkWilhelmDirkWilhelm Dirk Wilhelm USMember ✭✭✭
    Accepted Answer

    Everytime you are adding an item to an ObservableCollection the CollectionChanged event is triggered and the listview will be redrawn, so if you add a lot of items this can be really bad.

    Take a look at the ObservableRangeCollection in James Montemagnos MvvmHelpers:

    https://github.com/jamesmontemagno/mvvm-helpers

    https://channel9.msdn.com/Shows/XamarinShow/The-Xamarin-Show-12-MVVM-Helpers#time=14m00s

  • EdiporeiEdiporei Raelson Araújo BRMember ✭✭
    edited October 12

    @JulienRosen @DirkWilhelm you guys saved me some days of researching. Thanks a lot!

    @JulienRosen said:
    If you have a List, you iterate through it, and add each item individually to your OC.

    Just done that, it's now saving me from having a INPC on each OC. Thanks!

    @DirkWilhelm said:
    Take a look at the ObservableRangeCollection in James Montemagnos MvvmHelpers:

    Will check on that. Thanks!

    As for the breaking I was facing, it had nothing to do with the collections themselves, it was due to some missteps I had done on the Xaml code of this second page while I was adapting everything to this new approach. Having an iteration in a separate List helped me to see that the issue wasn't on the OCs. I guess it's a second good reason for this iterative approach instead of declaring a new OC as it helps the debugging.

Sign In or Register to comment.