user979331
user979331

Reputation: 11841

C# Xamarin Forms Populating CollectionView from ViewModel is always null

I am trying to populate a collection view from a ViewModel, however when I try to bind the data to the collection view, the ViewModel is null.

xaml.cs file

ObservableCollection<ReportsClass> newKidList = new ObservableCollection<ReportsClass>();

        public ReportsViewModel viewmodel { get; set; }

        public ReportsPage()
        {

            InitializeComponent();

            viewmodel = new ReportsViewModel();

            this.BindingContext = viewmodel;

            PreviousDateRange.CornerRadius = 20;
            NextDateRange.CornerRadius = 20;

            DateTime firstDate = currentDate.StartOfWeek(DayOfWeek.Sunday);
            DateTime secondDate = currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday);

            DateRange.Text = firstDate.ToString("MMMM d") + " - " + secondDate.ToString("MMMM d");

            Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(viewmodel.kids));


        }

Here is my view model

public class ReportsViewModel
    {

        public ObservableCollection<ReportsClass> kids { get; set; }

        FirebaseStorageHelper firebaseStorageHelper = new FirebaseStorageHelper();

        WebServiceClass webServiceClass = new WebServiceClass();

        DateTime currentDate = DateTime.Now;

        public ReportsViewModel()
        {
            GetKids();
        }

        public async void GetKids()
        {
            var parentId = await SecureStorage.GetAsync("parentid");

            kids = await webServiceClass.Reports(Convert.ToInt32(parentId), currentDate.StartOfWeek(DayOfWeek.Sunday), currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday));

        }
    }

And here is the method that gets the data for the view model

public async Task<ObservableCollection<ReportsClass>> Reports(int parentid, DateTime startDate, DateTime endDate)
        {
            var content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("parentid", parentid.ToString()),
                new KeyValuePair<string, string>("startDate", startDate.ToString("yyyy-MM-dd H:mm:ss")),
                new KeyValuePair<string, string>("endDate", endDate.ToString("yyyy-MM-dd"))
            });

            var response = await client.PostAsync(string.Format("https://example.com/api/index.php?action=reports"), content);

            var responseString = await response.Content.ReadAsStringAsync();

            ObservableCollection<ReportsClass> items = JsonConvert.DeserializeObject<ObservableCollection<ReportsClass>>(responseString);

            return items;

        }

What am I doing wrong? The purpose of me doing it this way is so I can update an item in the collectionview

Here is my ReportsClass

public class ReportsClass
{

    public ReportsClass(string firstName)
    {
        first_name = firstName;
    }

    public string first_name { get; set; }

}

Upvotes: 2

Views: 781

Answers (2)

ToolmakerSteve
ToolmakerSteve

Reputation: 21213

OPTION A:

  1. Fix the syntax of Kids.SetBinding, to not get null. Refer to the CLASS ReportsViewModel, not to the INSTANCE viewmodel:
    Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(ReportsViewModel.kids));
  1. The kids still won't appear in list. To fix, kids needs OnPropertyChanged:
    public ObservableCollection<ItemModel> kids {
        get => _kids;
        set {
            _kids = value;
            OnPropertyChanged();
        }
    }
    private ObservableCollection<ItemModel> _kids;
  1. See the other code in Option B. Adapt as desired.

  2. When you need XAML to see a DYNAMIC change, you need OnPropertyChanged. This is an implementation of INotifyPropertyChanged. Add this call to properties (that XAML binds to) of ReportsClass:

// Inheriting from `BindableObject` is one way to obtain OnPropertyChanged method.
public class ReportsClass : Xamarin.Forms.BindableObject
{

    public ReportsClass(string firstName)
    {
        first_name = firstName;
    }

    public string first_name {
        get => _first_name;
        set {
            _first_name = value;
            // This tells XAML there was a change.
            // Makes "{Binding first_name}" work dynamically.
            OnPropertyChanged();
        }
    }
    private string _first_name;

}

OPTION B:

Didn't find an answer anywhere that does everything correctly, so here is a complete sample, for future reference:

  1. Remove Kids.SetBinding(...). (It can be fixed as shown in OPTION A, but its easier to get it correct in XAML, so below I show it in XAML.)

  2. Bindings from Page to VM. See xaml below.

  3. Create ObservableCollection with setter that does OnPropertyChanged. This informs XAML when the list is ready, so page updates. (This is an implementation of INotifyPropertyChanged, as Jason mentioned.)

  4. Use Device.BeginInvokeOnMainThread(async () to create an async context, that is queued to run after constructor returns. (This fixes the issue Jason mentioned, which is that a constructor isn't an async context, so should not DIRECTLY call an async method such as QueryItemsAsync, or your GetKids.) This is more reliable.

PageWithQueryData.xaml:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TestXFUWP.PageWithQueryData">
    <ContentPage.Content>
        <StackLayout>
            <CollectionView ItemsSource="{Binding Items}">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <StackLayout>
                            <Label Text="{Binding Name}" />
                        </StackLayout>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
                <CollectionView.EmptyView>
                    <Grid>
                        <Label Text="Loading ..." FontSize="24" TextColor="Blue" BackgroundColor="LightBlue" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
                    </Grid>
                </CollectionView.EmptyView>
            </CollectionView>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

PageWithQueryData.xaml.cs:

public partial class PageWithQueryData : ContentPage
{
    public PageWithQueryData()
    {
        InitializeComponent();

        // ... other initialization work here ...
        // BUT remove `Kids.Binding(...);` line. See XAML: `ItemsSource="{Binding Items}"`.

        BindingContext = new VMWithQueryData();
    }
}

VMWithQueryData.cs:

class VMWithQueryData : Xamarin.Forms.BindableObject
{
    public VMWithQueryData()
    {
        // Start an async task to query.
        Xamarin.Forms.Device.BeginInvokeOnMainThread(async () => {
            await QueryItemsAsync();
        });

        // Alternative implementation: Start a background task to query.
        //QueryItemsInBackground();
    }

    public ObservableCollection<ItemModel> Items {
        get => _items;
        set {
            _items = value;
            OnPropertyChanged();
        }
    }
    private ObservableCollection<ItemModel> _items;


    private async Task QueryItemsAsync()
    {
        var names = new List<string> { "One", "Two", "Three" };
        bool queryOneAtATime = false;// true;
        if (queryOneAtATime) {
            // Show each item as it is available.
            Items = new ObservableCollection<ItemModel>();
            foreach (var name in names) {
                // Simulate slow query - replace with query that returns one item.
                await Task.Delay(1000);
                Items.Add(new ItemModel(name));
            }
        } else {
            // Load all the items, then show them.
            // Simulate slow query - replace with query that returns all data.
            await Task.Delay(3000);
            var items = new ObservableCollection<ItemModel>();
            foreach (var name in names) {
                items.Add(new ItemModel(name));
            }
            Items = items;
        }
    }

    // Alternative implementation, using a background thread.
    private void QueryItemsInBackground()
    {
        Task.Run(() => {
            var names = new List<string> { "One", "Two", "Three" };
            bool queryOneAtATime = false;// true;
            if (queryOneAtATime) {
                // Show each item as it is available.
                Items = new ObservableCollection<ItemModel>();
                foreach (var name in names) {
                    // Simulate slow query - replace with query that returns one item.
                    System.Threading.Thread.Sleep(1000);
                    Items.Add(new ItemModel(name));
                }
            } else {
                // Load all the items, then show them.
                // Simulate slow query - replace with query that returns all data.
                System.Threading.Thread.Sleep(3000);
                var items = new ObservableCollection<ItemModel>();
                foreach (var name in names) {
                    items.Add(new ItemModel(name));
                }
                Items = items;
            }
        });
    }
}

ItemModel.cs:

public class ItemModel
{
    public ItemModel(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

This also demonstrates <CollectionView.EmptyView> to display a message to user, while the data is being queried.

For completeness, I've included an alternative QueryItemsInBackground, that uses a background thread instead of an async method. Either approach works well.

Notice inheritance from Xamarin.Forms.BindableObject. This is one way to get an implementation of INotifyPropertyChanged. You can use any other MVVM library or technique.

Upvotes: 2

cahyo
cahyo

Reputation: 607

Move this line of code to the end of your constructor

this.BindingContext = viewmodel;

Upvotes: 1

Related Questions