Populating an observable collection in a ViewModel-Just learning MVVM

RobDurranceRobDurrance USMember ✭✭

Hello,
I am still learning Xamarin and I have successfully populated a ListView with an Observable collection from a sqlite DB, but only in code behind like this:

ObservableCollection<userTbl> userInfo = new ObservableCollection<userTbl>(await App.Database.GetUserAsync());
usrLst.ItemsSource = userInfo;

I now want to do this in the VM. One, because I may want this OC somewhere else in my app and Two, because this allows me to not only put it in a ListView, but maybe in some other controls. Three, I want to learn the MVVM method
This is what I have tried:

        private ObservableCollection<userTbl> _userInfo=new ObservableCollection<userTbl>(await App.Database.GetUserAsync());

        public ObservableCollection<userTbl> userInfo
                {
                    get
                    {

                        //_userInfo = App.Database.GetUserAsync(); I have also tried this instead of setting it at the declaration
                        return _userInfo;

                    }
                    set
                    {
                        _userInfo = value;
                        OnPropertyChanged();

                    }
                }

From what I have read, this should populate userInfo because I am setting it and returning it. This should in turn allow me bind it to my listView. However, I am getting an error about not being able to convert a task to an observable collection. I think that this is because in my data access layer the data call is:

public Task<List<userTbl>> GetUserAsync()
        {
            return _database.Table<userTbl>().ToListAsync();
        }

and there is a mismatch in the way that it is calling the data vs the way that the VM wants to receive it. The connection is a SQLiteAsyncConnection.
Here are my questions:
Should I make a new data call in my data class that does not use an async connection?
Should I create an async function in the VM to populate an observable collection and then assign it to _userTbl in the GET method? (I have seen examples of this by iterating through the list and literally populating am OC; but this seems like it creates more work for the App because it has to populate one list and then assign those values when my data call already creates a list.
Am I going about this all wrong and is there a better way?
Thank you,
Rob Durrance

Best Answers

Answers

  • JohnHardmanJohnHardman GBUniversity mod

    @LandLu said:

    Should I create an async function in the VM to populate an observable collection and then assign it to _userTbl in the GET method?

    Yes, you should call the async function in your view model's constructor to give your userInfo an initial value. Please notice that you must set the userInfo instead of _userInfo because of the OnPropertyChanged. It will notify your interface to respond to this changing behavior.

    public YourViewModel()
    {
        setUserInfo();
    }
    
    async void setUserInfo()
    {
        userInfo = new ObservableCollection<userTbl>(await App.Database.GetUserAsync());
    }
    

    Note that, assuming GetUserAsync does not execute synchronously, doing this will result in userInfo being null when the YourViewModel constructor completes, with userInfo only becoming non-null when GetUserAsync subsequently completes.

  • CharwakaCharwaka INMember ✭✭✭✭✭

    @JohnHardman said:

    @LandLu said:

    Should I create an async function in the VM to populate an observable collection and then assign it to _userTbl in the GET method?

    Yes, you should call the async function in your view model's constructor to give your userInfo an initial value. Please notice that you must set the userInfo instead of _userInfo because of the OnPropertyChanged. It will notify your interface to respond to this changing behavior.

    public YourViewModel()
    {
        setUserInfo();
    }
    
    async void setUserInfo()
    {
        userInfo = new ObservableCollection<userTbl>(await App.Database.GetUserAsync());
    }
    

    Note that, assuming GetUserAsync does not execute synchronously, doing this will result in userInfo being null when the YourViewModel constructor completes, with userInfo only becoming non-null when GetUserAsync subsequently completes.

    exactly.

    Task.Run(async () => _userInfo = new ObservableCollection(await App.Database.GetUserAsync()));

    this should do the job

  • LandLuLandLu Member, Xamarin Team Xamurai

    @JohnHardman It will. But I don't think it could be a problem as it will finally change the value after the data query has completed.
    Also, it has the same effect as your workaround @Charwaka. But you should set the userInfo's value.

  • JohnHardmanJohnHardman GBUniversity mod

    @Charwaka said:

    @JohnHardman said:

    @LandLu said:

    Should I create an async function in the VM to populate an observable collection and then assign it to _userTbl in the GET method?

    Yes, you should call the async function in your view model's constructor to give your userInfo an initial value. Please notice that you must set the userInfo instead of _userInfo because of the OnPropertyChanged. It will notify your interface to respond to this changing behavior.

    public YourViewModel()
    {
        setUserInfo();
    }
    
    async void setUserInfo()
    {
        userInfo = new ObservableCollection<userTbl>(await App.Database.GetUserAsync());
    }
    

    Note that, assuming GetUserAsync does not execute synchronously, doing this will result in userInfo being null when the YourViewModel constructor completes, with userInfo only becoming non-null when GetUserAsync subsequently completes.

    exactly.

    Task.Run(async () => _userInfo = new ObservableCollection(await App.Database.GetUserAsync()));

    this should do the job

    I don't see how that helps. _userInfo is still updated when GetUserAsync completes, so not necessarily before the constructor completes. The differences are that you are updating _userInfo rather than userInfo, and that _userInfo is updated from a non-UI thread. In a general case, it is likely that events would want to be fired, but being fired on the UI thread rather than a non-UI thread.

  • NMackayNMackay GBInsider, University mod
  • RobDurranceRobDurrance USMember ✭✭

    Thank you so much for the speedy response. The way that @Charwaka and the way that @LandLu both work as far as I can tell but I am not getting any data displayed in the ListView. Am I missing a step? I see that @JohnHardman mentioned that _userInfo would be null initially, which makes sense. Do I need to now put in a handler or something to tell it to update the view with the newly populated data once it gets loaded since it has "changed" since it was created and sent to the view? Or am I doing something wrong on the binding of the listView? Again, this is new to me and I am still trying to wrap my head around it all. Here is what I have so far
    Here is my VM:

    class userInfoVM:INotifyPropertyChanged
        {
            public userInfoVM()
            {
    
                Task.Run(async () => _UserInfo = new ObservableCollection<userTbl>(await App.Database.GetUserAsync()));
    
                //GetUserAsync();
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            private ObservableCollection<userTbl> _UserInfo;
    
            public ObservableCollection<userTbl> UserInfo
            {
                get
                {
                    return _UserInfo;
    
    
                }
                set
                {
                    _UserInfo = value;
                    OnPropertyChanged();
    
                }
            }
    
            private void OnPropertyChanged([CallerMemberName] string caller = "")
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
            }
    

    My ListView is attached because it goobers up the code:

  • CharwakaCharwaka INMember ✭✭✭✭✭
    edited April 23

    Hi @RobDurrance

    Make these Changes

    In ItemSource={Binding UserInfor,Mode=TwoWay}

    and

    Task.Run(async () =>UserInfo= new ObservableCollection(await App.Database.GetUserAsync()));

    Dont forget to mark as answer

  • LandLuLandLu Member, Xamarin Team Xamurai

    @RobDurrance As I said above, you have to set the value of UserInfo instead of _UserInfo. It will trigger the OnPropertyChanged evnent to notify the interface the value's changing.

  • JohnHardmanJohnHardman GBUniversity mod

    @RobDurrance

    Just as an aside,

    (1)

    If your setter currently looks like this:

    set
    {
        _UserInfo = value;
        OnPropertyChanged();
    }
    

    Change it to:

    set
    {
        if (value != _UserInfo)
        {
            _UserInfo = value;
            OnPropertyChanged();
        }
    }
    

    As you don't want to raise a PropertyChanged event if the property hasn't changed value.
    Note that you might want to use an EqualityComparer instead of !=. Or, for floating point properties, you might want to allow tiny differences to be treated as still being the same.

    (2)

    Your naming conventions (in terms of upper/lower case) are confusing. Typically, a class would be "UserTbl" rather than "userTbl", and a property called "UserInfo" would have a backing variable called "_userInfo".

    You might also want to avoid using abbreviations in class and property names, although different people have different opinions on that one.

  • RobDurranceRobDurrance USMember ✭✭

    I just want to say thank you everyone so much for helping. This is working perfectly and I now have a deeper knowledge of MVVM. @JohnHardman , thank you for the advice on naming conventions, old habits die hard. @LandLu , thank you for reminding me about which property to set the value on and giving me an additional option for execution. It must have been a typo. @Charwaka , I ended up using your implementation, thank you for that.
    The responses that I have received have been fantastic. I hope to contribute more in the future with the knowledge that I am gaining.

  • CharwakaCharwaka INMember ✭✭✭✭✭

    @RobDurrance said:
    I just want to say thank you everyone so much for helping. This is working perfectly and I now have a deeper knowledge of MVVM. @JohnHardman , thank you for the advice on naming conventions, old habits die hard. @LandLu , thank you for reminding me about which property to set the value on and giving me an additional option for execution. It must have been a typo. @Charwaka , I ended up using your implementation, thank you for that.
    The responses that I have received have been fantastic. I hope to contribute more in the future with the knowledge that I am gaining.

    yep its a typo i supposed to UserInfo instead of _UserInfo.

    Thanks to @JohnHardman for sharing his knowledge.

  • RobDurranceRobDurrance USMember ✭✭

    @Charwaka, I meant a typo on my end. I see that you corrected it in your second response. Also, while both will work, if I wanted to recognize that there was a change (which I do) I would need the public property. All good.

Sign In or Register to comment.