0

I want such type of ListView with different ViewCell types, different data, different header text (with custom header cell), but all in one ListView:

**********************
* General info
**********************
| Category: Cabrio
| Type: Sportscar
**********************
* Available models
**********************
| Year: 2007
| Manufacturer: Chevrolet
| Model: Corvette
----------------------
| Year: 2009
| Manufacturer: Dodge
| Model: Charger
----------------------

Therefore I use this Grouping object to hold the section (header) title and the list:

Grouping.cs:

public class Grouping<K, T> : ObservableCollection<T>
{
    public K Key { get; private set; }

    public Grouping(K key, IEnumerable<T> items)
    {
        Key = key;
        foreach (var item in items)
            this.Items.Add(item);
    }
}

Taken from this not available link.

How can I use different data in my ListView? Currently I have this non compiling code in my

MainPage.xaml.cs:

public partial class MainPage : ContentPage
{
    private ObservableCollection<Model.Grouping<string, object>> itemsGrouped;

    public MainPage()
    {
        InitializeComponent();
        
        this.itemsGrouped = new ObservableCollection<Grouping<string, object>>();

        List<Category> categories = new List<Category>();
        categories.Add(new Category("Cabrio", "Sportscar"));
        this.itemsGrouped.Add(new Grouping<string, Category>("General info", categories));

        List<CarInfo> cars = new List<CarInfo>();
        cars.Add(new CarInfo("2007", "Chevrolet", "Corvette"));
        cars.Add(new CarInfo("2009", "Dodge", "Charger"));
        this.itemsGrouped.Add(new Grouping<string, CarInfo>("Available models", cars));

        this.mainList.BindingContext = this.itemsGrouped;
    }
}

Argument 1: cannot convert from 'TestGroupedListView.Model.Grouping<string, TestGroupedListView.Model.Category>' to 'TestGroupedListView.Model.Grouping<string, object>'

The general idea is to use a DataTemplateSelector to be able to use different type of cells and use Grouping to have different custom header titles.

Here is my full example project code:

Category.cs:

public class Category
{
    public string CategoryName { get; set; }
    public string TypeOfCar { get; set; }


    public Category(string categoryName, string typeOfCar)
    {
        this.CategoryName = categoryName;
        this.TypeOfCar = typeOfCar;
    }
}

CarInfo.cs:

public class CarInfo
{
    public string Year { get; set; }
    public string Manufacturer { get; set; }
    public string Name { get; set; }

    public CarInfo(string year, string manufacturer, string name)
    {
        this.Year = year;
        this.Manufacturer = manufacturer;
        this.Name = name;
    }
}

GeneralView.xaml:

<?xml version="1.0" encoding="UTF-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TestGroupedListView.CustomView.GeneralView">
    
    <Grid x:Name="mainGrid" Padding="5" VerticalOptions="CenterAndExpand">


    </Grid>
    
</ViewCell>

GeneralView.xaml.cs:

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class GeneralView : ViewCell
{
    private List<KeyValuePair<string, string>> dataList;

    public GeneralView()
    {
        InitializeComponent();
    }

    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();

        var generalInfo = BindingContext as Category;

        if (generalInfo != null)
        {
            this.SetupView(generalInfo);
        }
    }

    private void SetupView(Category item)
    {
        this.dataList = new List<KeyValuePair<string, string>>();
        this.dataList.Add(new KeyValuePair<string, string>("Name", item.CategoryName));
        this.dataList.Add(new KeyValuePair<string, string>("Type", item.TypeOfCar));

        this.mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        this.mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

        for (int i = 0; i < dataList.Count; i++)
        {
            this.mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

            var keyLabel = new Label()
            {
                Text = dataList[i].Key
            };

            var valueLabel = new Label()
            {
                Text = dataList[i].Value
            };

            this.mainGrid.Children.Add(keyLabel, 0, i);
            this.mainGrid.Children.Add(valueLabel, 1, i);
        }
    }
}

InfoItemView.xaml:

<?xml version="1.0" encoding="UTF-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms" 
          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
          x:Class="TestGroupedListView.CustomView.InfoItemView">
    <Grid x:Name="mainGrid" Padding="5" VerticalOptions="CenterAndExpand">


    </Grid>
</ViewCell>

InfoItemView.xaml.cs:

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class InfoItemView : ViewCell
{
    private List<KeyValuePair<string, string>> dataList;

    public InfoItemView()
    {
        InitializeComponent();
    }

    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();

        var info = BindingContext as CarInfo;

        if (info != null)
        {
            this.SetupView(info);
        }
    }

    private void SetupView(CarInfo item)
    {
        this.dataList = new List<KeyValuePair<string, string>>();
        this.dataList.Add(new KeyValuePair<string, string>("Year", item.Year));
        this.dataList.Add(new KeyValuePair<string, string>("Manufacturer", item.Manufacturer));
        this.dataList.Add(new KeyValuePair<string, string>("Name", item.Name));

        this.mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        this.mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

        for (int i = 0; i < dataList.Count; i++)
        {
            this.mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

            var keyLabel = new Label()
            {
                Text = dataList[i].Key
            };

            var valueLabel = new Label()
            {
                Text = dataList[i].Value
            };

            this.mainGrid.Children.Add(keyLabel, 0, i);
            this.mainGrid.Children.Add(valueLabel, 1, i);
        }
    }
}

In my real project the two cell types differ more, but you should get the idea.

ListViewGroupHeader.cs:

public class ListViewGroupHeader : Label
{
}

MyDataTemplateSelector.cs:

public class MyDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate CarTemplate { get; set; }
    public DataTemplate GeneralTemplate { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        if (item is CarInfo)
        {
            return this.CarTemplate;
        }
        else if (item is Category)
        {
            return this.GeneralTemplate;
        }

        return new DataTemplate();
    }
}

MainPage.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:renderer="clr-namespace:TestGroupedListView.CustomRenderer;assembly=TestGroupedListView"
             xmlns:customViews="clr-namespace:TestGroupedListView.CustomView;assembly=TestGroupedListView"
             xmlns:local="clr-namespace:TestGroupedListView.Helper;assembly=TestGroupedListView"
             x:Class="TestGroupedListView.MainPage">

    <ContentPage.Resources>
        <ResourceDictionary>
            <DataTemplate x:Key="carTemplate">
                <customViews:InfoItemView />
            </DataTemplate>
            <DataTemplate x:Key="generalTemplate">
                <customViews:GeneralView />
            </DataTemplate>
            <local:MyDataTemplateSelector x:Key="myDataTemplateSelector" CarTemplate="{StaticResource carTemplate}" GeneralTemplate="{StaticResource generalTemplate}" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <ListView x:Name="mainList" 
              SeparatorColor="{StaticResource PrimaryLight}"
              HasUnevenRows="True"
              GroupDisplayBinding="{Binding Key}"
              IsGroupingEnabled="True"
              CachingStrategy="RecycleElement"
              ItemTemplate="{StaticResource myDataTemplateSelector}">

        
        <ListView.GroupHeaderTemplate>
            <DataTemplate>
                <renderer:ListViewGroupHeader Text="{Binding Key}" Style="{StaticResource labelHeader}" HeightRequest="40" />
            </DataTemplate>
        </ListView.GroupHeaderTemplate>
        
        <!--
        <ListView.ItemTemplate>
            <DataTemplate>
                <customViews:InfoItemView />
            </DataTemplate>
        </ListView.ItemTemplate>
        -->
    </ListView>

</ContentPage>

That's the idea. Would that work? Or should I look for another solution? I didn't find something in Google about the exact same problem ... (perhaps the wrong keywords?)

testing
  • 19,681
  • 50
  • 236
  • 417
  • If you want to use [DataTemplateSelector](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/templates/data-templates/selector),this enables multiple DataTemplates to be applied to the same type of object, to customize the appearance of particular objects. For ListView custom header, please take a look [customizing grouping](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance). I am not clear about your custom header, please give one screenshot about this. – Cherry Bu - MSFT Oct 22 '20 at 01:57
  • 1
    It makes more sense to use a [TableView](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/tableview) here. – FreakyAli Oct 22 '20 at 04:27
  • @CherryBu-MSFT: That is the question: Can you use different type of objects in `ListView`? The custom header is nothing special, but more than text and text color. There is a background color, some borders and so on. You can find a simple version in my answer below. – testing Oct 22 '20 at 10:22

1 Answers1

0

As FreakyAli mentioned TableView is an option. I completely forgot about this and therefore I made a minimal example.

MainPage.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"
             x:Class="TestGroupedListView.MainPage">

    <TableView x:Name="tableData" HasUnevenRows="True">
        
    </TableView>

</ContentPage>

MainPage.xaml.cs:

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

        this.tableData.Intent = TableIntent.Data;

        var category = new Category("Cabrio", "Sportscar");
        var generalView = new GeneralView();
        generalView.BindingContext = category;

        this.tableData.Root = new TableRoot()
        {
            new TableSection()
            {
                new ViewCell()
                {
                    View = new ListViewGroupHeader() 
                            {
                                Text = "General info",
                                HeightRequest = 40,
                                FontAttributes = FontAttributes.Bold,
                                TextColor = Color.White,
                                BackgroundColor = Color.Blue,
                                VerticalTextAlignment = TextAlignment.Center
                            }
                }
            },
            new TableSection()
            {
                generalView
            },
            new TableSection()
            {
                new ViewCell()
                {
                    View = new ListViewGroupHeader()
                            {
                                Text = "Available models",
                                HeightRequest = 40,
                                FontAttributes = FontAttributes.Bold,
                                TextColor = Color.White,
                                BackgroundColor = Color.Blue,
                                VerticalTextAlignment = TextAlignment.Center
                            }
                }
            },
        };

        List<CarInfo> cars = new List<CarInfo>();
        cars.Add(new CarInfo("2007", "Chevrolet", "Corvette"));
        cars.Add(new CarInfo("2009", "Dodge", "Charger"));

        foreach (var car in cars)
        {
            var carInfo = new InfoItemView();
            carInfo.BindingContext = car;
            carInfo.Tapped += CarInfo_Tapped;
            this.tableData.Root.Add(new TableSection() { carInfo });
        }

        
    }

    private void CarInfo_Tapped(object sender, EventArgs e)
    {
        var carInfo = (InfoItemView)sender;
        var car = (CarInfo)carInfo.BindingContext;
        System.Diagnostics.Debug.WriteLine(car.Name);
    }
}

For custom headers this comment helped me. It is possible to use a TableSection without the header text.

The result looks like this:

TableView with different TableSection

I have to rebuild my application to see if all criteria and special cases are met, but it looks promissing. But I still would love to know if you could use different types of objects in ListView ... Another alternative would be to use a StackLayout in a ScrollView and add the elements one by one.

testing
  • 19,681
  • 50
  • 236
  • 417