0
votes

ListViews follow the ItemPicker/Selector pattern of UI controls. Generally speaking, these types of controls, regardless of their platform will have a SelectedItem, and an ItemsSource. The basic idea is that there is a list of items in the ItemsSource, and the SelectedItem can be set to one of those items. Some other examples are ComboBoxes (Silverlight/UWP/WPF), and Pickers (Xamarin Forms).

In some cases, these controls are async ready, and in other cases, code needs to be written in order to handle scenarios where the ItemsSource is populated later than the SelectedItem. In our case, most of the time, the BindingContext (which contains the property bound to SelectedItem) will be set before the ItemsSource. So, we need to write code to allow this to function correctly. We have done this for ComboBoxes in Silverlight for example.

In Xamarin Forms, the ListView control is not async ready, i.e. if the ItemsSource is not populated before the SelectedItem is set, the selected item will never be highlighted on the control. This is probably by design and this is OK. The point of this thread is to find a way make the ListView async ready so that the ItemsSource can be populated after the SelectedItem is set.

There should be straight forward work arounds that can be implemented on other platforms to achieve this, but there are a few bugs in the Xamarin Forms list view that make it seemingly impossible to work around the issue. The sample app I have created is shared between WPF and Xamarin Forms in order to show how the ListView behaves differently on each platform. The WPF ListView for example, is async ready. If the ItemsSource is populated after the DataContext is set on a WPF ListView, the SelectedItem will bind to the item in the list.

In Xamarin Forms, I cannot consistently get SelectedItem two way binding on ListView to work. If I select an item in the ListView, it sets the property on my model, but if I set the property on my model, the item that should be selected is not reflected as being selected in the ListView. After the items have been loaded, when I set the property on my model, no SelectedItem is displayed. This is happening on UWP and Android. iOS remains untested.

You can see the sample problem in this Git repo: https://[email protected]/ChristianFindlay/xamarin-forms-scratch.git . Simply run the UWP, or Android sample, and click Async ListView. You can also run the XamarinFormsWPFComparison sample to see how the WPF version behaves differently.

When you run the Xamarin Forms sample, you will see that there is no item selected after the items load. However in the WPF version, it is selected. Note: It's not highlighted blue, but it is slightly grey indicating that it is selected. This is where my problem is, and the reason I can't work around the async issue.

Here is my code (clone repo for absolute latest code):

public class AsyncListViewModel : INotifyPropertyChanged
{
    #region Fields
    private ItemModel _ItemModel;
    #endregion

    #region Events
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region Public Properties
    public ItemModel ItemModel
    {
        get
        {
            return _ItemModel;
        }

        set
        {
            _ItemModel = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemModel)));
        }
    }
    #endregion
}


public class ItemModel : INotifyPropertyChanged
{
    #region Fields
    private int _Name;
    private string _Description;
    #endregion

    #region Events
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region Public Properties
    public int Name
    {
        get
        {
            return _Name;
        }

        set
        {
            _Name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public string Description
    {
        get
        {
            return _Description;
        }

        set
        {
            _Description = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description)));
        }
    }
    #endregion

    #region Public Methods
    public override bool Equals(object obj)
    {
        var itemModel = obj as ItemModel;
        if (itemModel == null)
        {
            return false;
        }

        var returnValue = Name.Equals(itemModel.Name);

        Debug.WriteLine($"An {nameof(ItemModel)} was tested for equality. Equal: {returnValue}");

        return returnValue;
    }

    public override int GetHashCode()
    {
        Debug.WriteLine($"{nameof(GetHashCode)} was called on an {nameof(ItemModel)}");
        return Name;
    }

    #endregion
}

public class ItemModelProvider : ObservableCollection<ItemModel>
{
    #region Events
    public event EventHandler ItemsLoaded;
    #endregion

    #region Constructor
    public ItemModelProvider()
    {
        var timer = new Timer(TimerCallback, null, 3000, 0);
    }
    #endregion

    #region Private Methods
    private void TimerCallback(object state)
    {
        Device.BeginInvokeOnMainThread(() => 
        {
            Add(new ItemModel { Name = 1, Description = "First" });
            Add(new ItemModel { Name = 2, Description = "Second" });
            Add(new ItemModel { Name = 3, Description = "Third" });
            ItemsLoaded?.Invoke(this, new EventArgs());
        });
    }
    #endregion
}

This is the XAML:

    <Grid x:Name="TheGrid">

        <Grid.Resources>
            <ResourceDictionary>
                <local:ItemModelProvider x:Key="items" />
            </ResourceDictionary>
        </Grid.Resources>

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

        <ListView x:Name="TheListView" Margin="4" SelectedItem="{Binding ItemModel, Mode=TwoWay}" ItemsSource="{StaticResource items}" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="#EEEEEE" >

            <ListView.ItemTemplate>
                <DataTemplate>

                    <ViewCell>

                        <Grid >

                            <Grid.RowDefinitions>
                                <RowDefinition Height="20" />
                                <RowDefinition Height="20" />
                            </Grid.RowDefinitions>

                            <Label Text="{Binding Name}" TextColor="#FF0000EE" VerticalOptions="Center"  />
                            <Label Text="{Binding Description}" Grid.Row="1"  VerticalOptions="Center" />

                        </Grid>


                    </ViewCell>

                </DataTemplate>
            </ListView.ItemTemplate>

        </ListView>

        <ActivityIndicator x:Name="TheActivityIndicator" IsRunning="True" IsVisible="True" Margin="100" />

        <StackLayout Grid.Row="1" Orientation="Horizontal">
            <Label Text="Name: " />
            <Label Text="{Binding ItemModel.Name}" />
            <Label Text="Description: " />
            <Label Text="{Binding ItemModel.Description}" />
            <Button Text="New Model" x:Name="NewModelButton" />
            <Button Text="Set To 2" x:Name="SetToTwoButton" />
        </StackLayout>

    </Grid>

Code Behind:

public partial class AsyncListViewPage : ContentPage
{
    ItemModelProvider items;
    ItemModel two;

    private AsyncListViewModel CurrentAsyncListViewModel => BindingContext as AsyncListViewModel;

    public AsyncListViewPage()
    {
        InitializeComponent();

        CreateNewModel();

        items = (ItemModelProvider)TheGrid.Resources["items"];
        items.ItemsLoaded += Items_ItemsLoaded;

        NewModelButton.Clicked += NewModelButton_Clicked;
        SetToTwoButton.Clicked += SetToTwoButton_Clicked;
    }

    private void SetToTwoButton_Clicked(object sender, System.EventArgs e)
    {
        if (two == null)
        {
            DisplayAlert("Wait for the items to load", "Wait for the items to load", "OK");
            return;
        }

        CurrentAsyncListViewModel.ItemModel = two;
    }

    private void NewModelButton_Clicked(object sender, System.EventArgs e)
    {
        CreateNewModel();
    }

    private void CreateNewModel()
    {
        //Note: if you replace the line below with this, the behaviour works:
        //BindingContext = new AsyncListViewModel { ItemModel = two };

        BindingContext = new AsyncListViewModel { ItemModel = GetNewTwo() };
    }

    private static ItemModel GetNewTwo()
    {
        return new ItemModel { Name = 2, Description = "Second" };
    }

    private void Items_ItemsLoaded(object sender, System.EventArgs e)
    {
        TheActivityIndicator.IsRunning = false;
        TheActivityIndicator.IsVisible = false;
        two = items[1];
    }
}

Note: if I change the method CreateNewModel to this:

    private void CreateNewModel()
    {
        BindingContext = new AsyncListViewModel { ItemModel = two };
    }

the SelectedItem is reflected on screen. This seems to indicate that the ListView is comparing items based on object reference as opposed to using the Equals method on the objects. I tend to think of this as a bug. But, this is not the only issue here, because if this were the only issue, then clicking the SetToTwoButton should yield the same result.

It is now clear that there are several bugs around this is Xamarin Forms. I have documented the repro steps here: https://bugzilla.xamarin.com/show_bug.cgi?id=58451

2
I'm sorry, but I doubt there is a bug in here. I have used this extensively and have never encountered any problems. First; you set the SelectedItem to a property called ItemModel, but I don't see that property anywhere in the code you have provided? - Gerald Versluis
Open the bug here bugzilla.xamarin.com/show_bug.cgi?id=58451 and follow the steps to reproduce. - Christian Findlay
I've added the missing code, but all the code is in the repository mentioned which is the easiest way to see the bug recreated. - Christian Findlay
Why are you dismissing this bug without even running the sample @GeraldVersluis? There's no reason that a control that has worked in the past can't get a bug from a change in the Xamarin.Forms code base. This issue is easily reproducible. - The Pademelon

2 Answers

0
votes

The AdaptListView is a suitable alternative to the ListView control and isn't subject to these issues.

0
votes

The Xamarin Forms team created a pull request to solve some of the issues here: https://github.com/xamarin/Xamarin.Forms/pull/1152

But, I don't believe this pull request was ever accepted in to the master branch of Xamarin Forms.