Tim
Tim

Reputation: 5

Xamarin Forms ListView Grouping Contents not Binding

I am having issues using ListView Grouping to get the content of my lists to display in labels. The ListView's ItemsSource and GroupDisplayBinding are setting properly, but the label inside the ListView will not display anything (I even tried setting it to a literal string). All of my lists, as well as my "list of lists" are populating correctly. I have poured over the documentation, website articles, and instructional videos but I am still stuck. Can someone point me in the right direction? I am still having a tough time with databinding and MVVM in general. Thanks.

View:

<ListView ItemsSource="{Binding OAllLists}"
              GroupDisplayBinding="{Binding Type}"
              IsGroupingEnabled="true"
              HasUnevenRows="True">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Label Text="{Binding Items}">
                </Label>
            </DataTemplate>
        </ListView.ItemTemplate>
</ListView>

ViewModel:

class OrganizedViewModel
{
    public ObservableCollection<Categories> OAllLists { get; set; }

    public OrganizedViewModel()
        {
        OAllLists = new ObservableCollection<Categories>();
                foreach(Categories value in Categories.AllLists)
                {
                    OAllLists.Add(value);
                }
        }
} 

Model:

public class Categories
    {
        public static List<Categories> AllLists { get; set; } = new List<Categories>();
        
        public static Categories FruitList { get; set; } = new Categories("Fruit");
        public static Categories VegetableList { get; set; } = new Categories("Vegetables");
        ///Each type of item has its own static Categories object

        public string Type { get; set; }
        public List<string> Items { get; set; } = new List<string>();
    }

Method for organizing items:

class OrganizeIt
{
    public void OrganizeItems(List<string> groceries)
    {
        foreach (string value in groceries) ///Checks each value for keywords
        {
            if (Keywords.fruitKeys.Any(value.Contains))
            {
                Categories.FruitList.Items.Add(value);
            }
            else if (Keywords.vegetableKeys.Any(value.Contains))
            {
                Categories.VegetableList.Items.Add(value);
            }
        }

        ///Adds each type of list to "list of lists" if it contains values
        if (Categories.FruitList.Items.Any())
        {
            Categories.AllLists.Add(FruitItem);
        }
        if (Categories.VegetableList.Items.Any())
        {
            Categories.AllLists.Add(Categories.VegetableList);
        }

Edit

New class per comment's recommendation. I also created another Observable Collection in the ViewModel populated by GroupedList (both are working correctly). Name changes are for clarity.

public class Groceries : List<string>
    {
        public string Category { get; set; }

        public static List<Groceries> GroupedList { get; set; } = new List<Groceries>();
        public static Groceries Fruits { get; set; } = new Groceries("Fruit");
        public static Groceries Vegetables { get; set; } = new Groceries("Vegetables");

        public Groceries(string s)
        {
            Category = s;
        }
    }

New View:

<ListView ItemsSource="{Binding OGroupedList}"
                  GroupDisplayBinding="{Binding Category}"
                  IsGroupingEnabled="true">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding .}"
                               VerticalOptions="FillAndExpand"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

Edit 2

This is how I'm populating the ObservableCollection in my ViewModel now:

class OrganizedViewModel
    {
        public ObservableCollection<Groceries> OGroupedList { get; set; }
        public string Category { get; set; }

         public OrganizedViewModel()
        {
            OGroupedList = new ObservableCollection<Groceries>();
            foreach (Groceries value in Groceries.GroupedList)
            {
                OGroupedList.Add(value);
            }
    }

Edit 3

This is the method for organizing items. It takes in a string list and checks each list item to see if it contains any of the keywords associated with a certain category (ex. "apple" is contained in "2 bags of apples"). If so, the list item is added to the corresponding Groceries object.

class OrganizeIt
{
    public void OrganizeItems(List<string> groceries)
    {
        foreach (string value in groceries)
        {
            if (Keywords.fruitKeys.Any(value.Contains))
            {
                Groceries.Fruits.Add(value);
            }
            else if (Keywords.vegetableKeys.Any(value.Contains))
            {
                Groceries.Vegetables.Add(value);
            }
    }
    if (Groceries.Fruits.Any())
    {
        Groceries.GroupedList.Add(Groceries.Fruits);
    }
    if (Groceries.Vegetables.Any())
    {
        Groceries.GroupedList.Add(Groceries.Vegetables);
    }
}

Here is where the method is called on the MainPage. UnorganizedList is populated from user input.

private void SortItems()
{
    OrganizeIt o = new OrganizeIt();
    o.OrganizeItems(UnorganizedList);
}

Solution

All that was needed was to change the Label's binding to just {Binding} (with no period), as well as remove the "x:DataType" line. Below in the revised View in case this helps anybody:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewmodels="clr-namespace:GroceryListMobile.ViewModels"
             xmlns:mvvm="clr-namespace:MvvmHelpers;assembly=MvvmHelpers"
             xmlns:model="clr-namespace:GroceryListMobile.Models"
             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
             
             x:Class="GroceryListMobile.Views.OrganizedView"
             x:Name="Organized">
    <ContentPage.BindingContext>
        <viewmodels:OrganizedViewModel/>
    </ContentPage.BindingContext>

    <ContentPage.Content>

        <ListView ItemsSource="{Binding OGroupedList}"
                  GroupDisplayBinding="{Binding Category}"
                  IsGroupingEnabled="true">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding}"
                               VerticalOptions="FillAndExpand"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage.Content>
</ContentPage>

Upvotes: 0

Views: 1105

Answers (2)

DavidS
DavidS

Reputation: 2934

You need a <ViewCell> as the child node of your <DataTemplate>:

<ListView ItemsSource="{Binding OAllLists}"
              GroupDisplayBinding="{Binding Type}"
              IsGroupingEnabled="true"
              HasUnevenRows="True">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <Label Text="{Binding .}">
                    </Label>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
</ListView>

ListView.ItemTemplate always requires a DataTemplate which is a Cell.

EDIT The next thing to address is the collection used as the ItemSource. When using grouping, that needs to be a collection of collections. The thing I would try is to change your model class:

public class Categories : List<string>
    {
        public static List<Categories> AllLists { get; set; } = new List<Categories>();
        
        public static Categories FruitList { get; set; } = new Categories("Fruit");
        public static Categories VegetableList { get; set; } = new Categories("Vegetables");
        ///Each type of item has its own static Categories object

        public string Type { get; set; }
    }

And see the revised Binding in the Xaml above.

A way to think about grouped ListViews is that it is a list of lists. The "outer" list elements have a member bound to GroupDisplayBinding, and the "inner" list elements have members bound to the elements in the DataTemplate.

In your case, the "outer" collection is the ObservableCollection, so the GroupDisplayBinding will bind to something on Categories. Then each Categories needs to be a collection itself. In the revisions, that is a List, so the DataTemplate is given a string as the BindingContext. So the Label just needs to bind to the string (hence the Binding ..

The rest of the code would have to adjust a bit as the Items collection that used to be a member of Categories is now the Categories object itself (through inheritance).

EDIT 2

I created a new app with a page containing the ListView exactly as you have it (New View under your first Edit), and the Groceries class exactly as it is under the first Edit.

I revised OrganizedViewModel a little. It doesn't seem to use Category, and I wanted to make sure OGroupedList was being populated with Categories:

    class OrganizedViewModel
    {
        public ObservableCollection<Groceries> OGroupedList { get; set; }

        public OrganizedViewModel()
        {
            OGroupedList = new ObservableCollection<Groceries>();
            OGroupedList.Add(Groceries.Fruits);
            OGroupedList.Add(Groceries.Vegetables);
        }
    }

Finally, I added some items to each category in the page's constructor when creating the page:

        public Page1()
        {
            InitializeComponent();

            var bc = new OrganizedViewModel();
            var index = 1;
            foreach (var g in bc.OGroupedList)
            {
                g.Add(g.Category + $" {index++}");
                g.Add(g.Category + $" {index++}");
                g.Add(g.Category + $" {index++}");
                g.Add(g.Category + $" {index++}");
            }

            BindingContext = bc;
        }

And for me this is showing the lists with the item names and the group headers correctly. So whatever the problem is you're still seeing is somewhere else. The basic class structure and Xaml definition for the grouped ListView is now correct.

My two guesses:

  1. Make sure the collections are being populated correctly.
  2. Try changing to public class Groceries : ObservableCollection<string>. This is important if the Groceries list can change after the page is initially rendered.

EDIT 3

Got to the bottom of it. Here are comments that should get you going:

  1. There are subtle differences between {Binding .} and {Binding}. In your case, {Binding} works but {Binding .} does not.

  2. A more common case is where the elements are objects, not strings, so something like this also solves it:

public class GroceryItem
{
    public string Name { get; set; }
}
...
public class Groceries: ObservableCollection<GroceryItem>
{
    ...

This will, of course, require changes in adding to the Groceries collection and the Label needs to be Text="{Binding Name}".

  1. When I tried #2, Visual Studio was causing problems with x:DataType after making that change. Its detection of binding contexts in the IDE is not the best, so I had to delete that line.

  2. Be careful when using static collections here. If you add items, organize, go back, and organize again, the app will crash because it tries to re-add the fruits and vegetables collections.

Upvotes: 0

Wen xu Li
Wen xu Li

Reputation: 1686

According to your code, for the OAllLists to be displayed, you can create OAllLists like this:

OAllLists = new ObservableCollection<Categories>
{ 
    new Categories("FruitList"){"aaa","bbb","ccc},
    new Categories("VegetableList"){"ddd","eee"} 
};

Among them, "FruitList" is the type in the Categories class, and "aaa" is the string array you added in it. You can check this link for details (https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance#grouping)

Upvotes: 0

Related Questions