Home > Enterprise >  Xamarin passing list data from view model for view binding
Xamarin passing list data from view model for view binding

Time:10-24

New to Xamarin and in my code samples below I am trying to figure out how to pass Players from my view model for binding in view. I've confirmed GetPlayers() gets the correct data. Just not sure what updates I need to make to PlayersViewModel to pass the List players for binding. Thanks in advance!

PlayersViewModel.cs:

namespace App.ViewModels
{
    public class PlayersViewModel : BaseViewModel
    {

        public ICommand GetPlayersCommand { get; set; }

        public PlayersViewModel()
        {
            Title = "Players";
            GetPlayersCommand = new Command(async () => await GetPlayers());
            GetPlayersCommand.Execute(null);
        }

        private readonly string _yearDateFormat = "yyyy";

        public List<Player> Players { get; private set; } = new List<Player>();


        async Task GetPlayers()
        {
            IsRefreshing = true;

            List<Player> players = await ApiManager.GetPlayersAsync(DateTime.Now.Year.ToString(_yearDateFormat));

            IsRefreshing = false;
        }
    }

}

PlayersPage.xaml.cs

App.Views
{

    [DesignTimeVisible(false)]
    public partial class PlayersPage : ContentPage
    {
        readonly PlayersViewModel viewModel;

        public PlayersPage()
        {
            InitializeComponent();
            BindingContext = viewModel = new PlayersViewModel();
        }
    }
}

PlayersPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
             mc:Ignorable="d"
             x:Class="App.Views.PlayersPage"
             Title="{Binding Title}"
             x:Name="BrowsePlayersPage">
    <ListView x:Name="PlayersView" ItemsSource="{Binding Players}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding FirstName}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

CodePudding user response:

You are binding to the Players property which has never been set to anything.

Try to call InitData() in the constructor or anywhere you like:

public PlayersViewModel()
{
    Title = "Players";
    GetPlayersCommand = new Command(async () => await GetPlayers());
    GetPlayersCommand.Execute(null);

    InitData();
}

private async void InitData()
{
    Players = await GetPlayers();
}

CodePudding user response:

sabsab's answer does show the essence of what is wrong (OP did not set Players, so it was empty.)

However, you might encounter subtle issues if you attempt to do long-running operations while constructing PlayersViewModel.

It is safest to use one of two strategies:

  1. Show the page without the Players, then add them as they become available. (With a slight change, can wait to add them all at once.)

  2. Do the slow async operation BEFORE creating PlayersViewModel. Pass it in as a parameter to the constructor.


Solution 1:

public partial class PlayersPage : ContentPage
{
    public PlayersPage(PlayersViewModel vm = null)
    {
        InitializeComponent();

        if (vm == null)
            vm = PlayersViewModel.CreateDeferred();

        BindingContext = vm;
    }
}

public class PlayersViewModel : BindableObject
{
    /// <summary>
    /// Players starts empty. It is filled on a background thread.
    /// This allows page to display quickly, though at first it lacks Players.
    /// </summary>
    /// <returns></returns>
    public static PlayersViewModel CreateDeferred()
    {
        PlayersViewModel vm = new PlayersViewModel();
        // Empty list for initial display.
        vm.Players = new ObservableCollection<Player>();

        // This version runs on Main Thread.
        Device.BeginInvokeOnMainThread(async () => await vm.GetPlayers());

        // Returns before GetPlayers is finished.
        return vm;
    }

    public ObservableCollection<Player> Players { get; set; }

    /// <summary>
    /// "private". Only call this via static method.
    /// </summary>
    private PlayersViewModel()
    {
    }

    /// <summary>
    /// This touches Players, which is UI-visible, so only call from Main Thread.
    /// </summary>
    /// <returns></returns>
    async Task GetPlayers()
    {
        Players.Clear();

        for (int iPlayer = 0; iPlayer < 2; iPlayer  )
        {
            // Simulate query time.
            await Task.Delay(2000);
            Player player = new Player();
            Players.Add(player);
        }
    }
}

Alternative for Solution 1, that does not show until all players are fetched. This uses "PrefetchPlayers" from Solution 2:

    // Alternative version, that waits until all players are fetched.
    async Task GetPlayers()
    {
        List<Player> players = await PrefetchPlayers();
        Players = new ObservableCollection<Player>(players);
        OnPropertyChanged(nameof(Players));
    }

Solution 2:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        // This shows when app starts, while your data is loading.
        //TODO MainPage = new MyLoadingPage();
    }

    protected override async void OnStart()
    {
        List<Player> players = await PlayersViewModel.PrefetchPlayers();
        MainPage = new PlayersPage(new PlayersViewModel(players));
    }
}

public partial class PlayersPage : ContentPage
{
    public PlayersPage(PlayersViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm;
    }
}

public class PlayersViewModel : BindableObject
{
    public static async Task<List<Player>> PrefetchPlayers()
    {
        var players = new List<Player>();

        for (int iPlayer = 0; iPlayer < 2; iPlayer  )
        {
            // Simulate query time.
            await Task.Delay(2000);
            Player player = new Player();
            players.Add(player);
        }

        return players;
    }

    public ObservableCollection<Player> Players { get; set; }

    public PlayersViewModel(List<Player> players)
    {
        Players = new ObservableCollection<Player>(players);
    }
}
  • Related