Cedric Royer-Bertrand
Cedric Royer-Bertrand

Reputation: 741

MAUI How to bind an ObservableCollection to a Grid?

I have been able bind a Grid (size 2x2) to an ObservableCollection (size 4) but the code has a lot of duplication has each cell is manually binded to an index of the collection.

I need to increase the size of the grit to 7x7 so I would prefer a better way to do that. I've looked at:

but dataTemplate seems to work on CollectionView not a Grid and the ContentView need binding for each Parameter which does not reduce complexity much.

Does anyone has an idea / suggestion to improve this?

TestPage

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Views.TestPage"
             xmlns:viewmodels="clr-namespace:Maui.ViewModels"
             x:DataType="viewmodels:TestPageViewModel"
             Title="TestMap">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Frame Grid.Row="0"
               Grid.Column="0" 
               BackgroundColor="Blue">
            <StackLayout>
                <Label Text="{Binding Persons[0].Name}" />
                <Label Text="{Binding Persons[0].Age}" />
                <Label Text="{Binding Persons[0].Location}" />
            </StackLayout>
        </Frame>
        <Frame Grid.Row="0"
               Grid.Column="1" 
               BackgroundColor="Red">
            <StackLayout>
                <Label Text="{Binding Persons[1].Name}" />
                <Label Text="{Binding Persons[1].Age}" />
                <Label Text="{Binding Persons[1].Location}" />
            </StackLayout>
        </Frame>
        <Frame Grid.Row="1"
               Grid.Column="0" 
               BackgroundColor="Green">
            <StackLayout>
                <Label Text="{Binding Persons[2].Name}" />
                <Label Text="{Binding Persons[2].Age}" />
                <Label Text="{Binding Persons[2].Location}" />
            </StackLayout>
        </Frame>
        <Frame Grid.Row="1"
               Grid.Column="1" 
               BackgroundColor="Yellow">
            <StackLayout>
                <Label Text="{Binding Persons[3].Name}" />
                <Label Text="{Binding Persons[3].Age}" />
                <Label Text="{Binding Persons[3].Location}" />
            </StackLayout>
        </Frame>
    </Grid>  
</ContentPage>

TestPageViewModel

public partial class TestPageViewModel : ObservableObject
{
    public ObservableCollection<Person> Persons { get; set; }

    public TestPageViewModel()
    {
        Persons = new ObservableCollection<Person>
        {
            new Person { Name="John", Age=21, Location="US" },
            new Person { Name="Harry", Age=45, Location="Ireland" },
            new Person { Name="Robert", Age=32, Location="France" },
            new Person { Name="Juan", Age=68, Location="Spain" },
        };
    }
}

Model

public partial class Person : ObservableObject
{
    [ObservableProperty]
    public string name;
    [ObservableProperty]
    public int age;
    [ObservableProperty]
    public string location;
}

Upvotes: 0

Views: 2143

Answers (2)

Julian
Julian

Reputation: 8914

Since Grid doesn't support binding to collections for dynamic population, neither by exposing an ItemsSource property nor by using BindableLayout.ItemsSource, all you're left with is using another type of layout for the collection, such as a VerticalStackLayout, FlexibleLayout or a CollectionView (I recommend avoiding ListView for this) and then using a Grid inside of the DataTemplate for each row.

In your case I recommend using the CollectionView, because it allows different layout styles, such as a Grid with two columns (which seems to be what you're trying to achieve based on your code):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Views.TestPage"
             xmlns:viewmodels="clr-namespace:Maui.ViewModels"
             x:DataType="viewmodels:TestPageViewModel"
             Title="TestMap">

    <CollectionView
        ItemsSource="{Binding Persons}"
        ItemsLayout="VerticalGrid, 2">

        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="models:Person">
                <controls:PersonView Person="{Binding .}"/>

                <!-- OR -->
                <!--
                <Frame
                    BackgroundColor="Green">
                    <StackLayout>
                        <Label Text="{Binding Name}" />
                        <Label Text="{Binding Age}" />
                        <Label Text="{Binding Location}" />
                    </StackLayout>
                </Frame>
                -->
            </DataTemplate>
        </CollectionView.ItemTemplate>

    </CollectionView>

</ContentPage>

Upvotes: 0

Cedric Royer-Bertrand
Cedric Royer-Bertrand

Reputation: 741

Thanks Jason, I followed your advice and I have created a custom view. There are still some duplicate code but it's much more manageable now and less error prone.

I'm posting my code here for future dev who might need this. Updvote if it helped you :D

PersonView

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Controls.PersonView"
             x:Name="this">
    <Frame BindingContext="{x:Reference this}"
           BackgroundColor="{Binding BackgroundColor}">
        <StackLayout>
            <Label Text="{Binding Person.Name}" />
            <Label Text="{Binding Person.Age}" />
            <Label Text="{Binding Person.Location}" />
        </StackLayout>
    </Frame>
</ContentView>
public partial class PersonView : ContentView
{
    public static readonly BindableProperty PersonProperty = BindableProperty.Create(nameof(Person), typeof(Person), typeof(PersonView), new Person());
    public Person Person
    {
        get => (Person)GetValue(PersonView.PersonProperty);
        set => SetValue(PersonView.PersonProperty, value);
    }
    public PersonView()
    {
        InitializeComponent();
    }
}

Test Page

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Views.TestPage"
             xmlns:viewmodels="clr-namespace:Maui.ViewModels"
             x:DataType="viewmodels:TestPageViewModel"
             Title="TestMap"
             xmlns:controls="clr-namespace:Maui.Controls">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <controls:PersonView Person="{Binding Persons[0]}" BackgroundColor="Blue" Grid.Row="0" Grid.Column="0" />
        <controls:PersonView Person="{Binding Persons[1]}" BackgroundColor="Red" Grid.Row="0" Grid.Column="1" />
        <controls:PersonView Person="{Binding Persons[2]}" BackgroundColor="Green" Grid.Row="1" Grid.Column="0" />
        <controls:PersonView Person="{Binding Persons[3]}" BackgroundColor="Yellow" Grid.Row="1" Grid.Column="1" />
    </Grid>  
</ContentPage>

TestPageViewModel

using CommunityToolkit.Mvvm.ComponentModel;
using Maui.Models;
using System.Collections.ObjectModel;

namespace Maui.ViewModels;

public partial class TestPageViewModel : ObservableObject
{
    public ObservableCollection<Person> Persons { get; set; }

    public TestPageViewModel()
    {
        Persons = new ObservableCollection<Person>
        {
            new Person { Name="John", Age=21, Location="US" },
            new Person { Name="Harry", Age=45, Location="Ireland" },
            new Person { Name="Robert", Age=32, Location="France" },
            new Person { Name="Juan", Age=68, Location="Spain" },
        };
    }
}

Model

public partial class Person : ObservableObject
{
    [ObservableProperty]
    public string name;
    [ObservableProperty]
    public int age;
    [ObservableProperty]
    public string location;
}

Upvotes: 0

Related Questions