How to use a Grid with Bindable Layout (more than one column)
Asked Answered
X

5

6

In Xamarin.Forms 3.5 Microsoft introduced us to bindable layouts which can be used to dynamically fill layouts (e.g. StackLayout, Grid, etc.).

To use this in a grid with a single column is pretty straightforward:

<Grid BindableLayout.ItemsSource="{Binding Items}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <Label Text="{Binding MyProperty}"/>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</Grid>

Now my question is how this can be used to populate a grid with more than one column due to the fact that DataTemplate only allows one view as content. Sure I could but another Grid in it but this would totally nullify the value of bindable layout in a Grid.

Xanthippe answered 29/8, 2019 at 13:40 Comment(1)
So, how is that "pretty straightforward" even with single Grid column? How can you set view Grid.Row attribute in Data template for sequential source items? All your Labels overlaps in first row! – Magnetostriction
M
3

I've created a behaviour which provides view index as a bindable property. In my case I have always the same amount of items, so I can setup ColumnDefinitions/RowDefinitions and bind Grid.Column/Row to the behavior's Index property.

    public sealed class IndexProviderBehavior : Behavior<View>
    {
        View _view;

        public static readonly BindableProperty IndexProperty =
        BindableProperty.Create(nameof(Index), typeof(int), typeof(IndexProviderBehavior),
            defaultBindingMode: BindingMode.OneWayToSource);

        public int Index
        {
            get => (int)GetValue(IndexProperty);
            set => SetValue(IndexProperty, value);
        }

        protected override void OnAttachedTo(View bindable)
        {
            base.OnAttachedTo(bindable);
            _view = bindable;
            bindable.ParentChanged += OnParentChanged;
            SetupIndex();
        }

        protected override void OnDetachingFrom(View bindable)
        {
            base.OnDetachingFrom(bindable);
            _view.ParentChanged -= OnParentChanged;
            _view = null;
        }

        private void OnParentChanged(object sender, EventArgs e)
        {
            SetupIndex();
        }

        private void SetupIndex()
        {
            if (_view.Parent is Layout layout)
            {
                Index = layout.Children.IndexOf(_view);
                return;
            }

            Index = 0;
        }
    }

Usage:

            <Grid
                ColumnDefinitions="*,*,*,*,*,*,*"
                BindableLayout.ItemsSource="{Binding Items}">
                <BindableLayout.ItemTemplate>
                    <DataTemplate>
                        <Label
                            Text="{Binding .}"
                            Grid.Column="{Binding Index, Source={x:Reference indexBehavior}}"
                            >
                            <Label.Behaviors>
                                <behaviors:IndexProviderBehavior x:Name="indexBehavior" />
                            </Label.Behaviors>
                        </Label>
                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </Grid>
Merrell answered 18/8, 2023 at 12:53 Comment(1)
I don't know exactly but you could maybe even set the Grid.Column/Grid.Row from directly from the behavior (like Grid.ColumnProperty.SetValue or such), but just as a proof-of-concept this already is good πŸ‘ – Xanthippe
D
3

Now my question is how this can be used to populate a grid with more than one column due to the fact that DataTemplate only allows one view as content.

From Bindable Layouts, we can see:

While it's technically possible to attach a bindable layout to any layout class that derives from the Layout class, it's not always practical to do so, particularly for the AbsoluteLayout, Grid, and RelativeLayout classes. For example, consider the scenario of wanting to display a collection of data in a Grid using a bindable layout, where each item in the collection is an object containing multiple properties. Each row in the Grid should display an object from the collection, with each column in the Grid displaying one of the object's properties. Because the DataTemplate for the bindable layout can only contain a single object, it's necessary for that object to be a layout class containing multiple views that each display one of the object's properties in a specific Grid column. While this scenario can be realised with bindable layouts, it results in a parent Grid containing a child Grid for each item in the bound collection, which is a highly inefficient and problematic use of the Grid layout.

If you still want to more column, I suggest you can use StackLayout, it can also meet your requirement.

<StackLayout BindableLayout.ItemsSource="{Binding persons}">
        <BindableLayout.ItemTemplate>
            <DataTemplate>
                <StackLayout Orientation="Horizontal">
                    <Label Text="{Binding name}" />
                    <Label Text="{Binding age}" />
                </StackLayout>
            </DataTemplate>
        </BindableLayout.ItemTemplate>
    </StackLayout>
Divest answered 30/8, 2019 at 6:22 Comment(1)
that will not align items evenly if you want to have something like Grid with ColummDefinitions="," – Pavement
M
3

I've created a behaviour which provides view index as a bindable property. In my case I have always the same amount of items, so I can setup ColumnDefinitions/RowDefinitions and bind Grid.Column/Row to the behavior's Index property.

    public sealed class IndexProviderBehavior : Behavior<View>
    {
        View _view;

        public static readonly BindableProperty IndexProperty =
        BindableProperty.Create(nameof(Index), typeof(int), typeof(IndexProviderBehavior),
            defaultBindingMode: BindingMode.OneWayToSource);

        public int Index
        {
            get => (int)GetValue(IndexProperty);
            set => SetValue(IndexProperty, value);
        }

        protected override void OnAttachedTo(View bindable)
        {
            base.OnAttachedTo(bindable);
            _view = bindable;
            bindable.ParentChanged += OnParentChanged;
            SetupIndex();
        }

        protected override void OnDetachingFrom(View bindable)
        {
            base.OnDetachingFrom(bindable);
            _view.ParentChanged -= OnParentChanged;
            _view = null;
        }

        private void OnParentChanged(object sender, EventArgs e)
        {
            SetupIndex();
        }

        private void SetupIndex()
        {
            if (_view.Parent is Layout layout)
            {
                Index = layout.Children.IndexOf(_view);
                return;
            }

            Index = 0;
        }
    }

Usage:

            <Grid
                ColumnDefinitions="*,*,*,*,*,*,*"
                BindableLayout.ItemsSource="{Binding Items}">
                <BindableLayout.ItemTemplate>
                    <DataTemplate>
                        <Label
                            Text="{Binding .}"
                            Grid.Column="{Binding Index, Source={x:Reference indexBehavior}}"
                            >
                            <Label.Behaviors>
                                <behaviors:IndexProviderBehavior x:Name="indexBehavior" />
                            </Label.Behaviors>
                        </Label>
                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </Grid>
Merrell answered 18/8, 2023 at 12:53 Comment(1)
I don't know exactly but you could maybe even set the Grid.Column/Grid.Row from directly from the behavior (like Grid.ColumnProperty.SetValue or such), but just as a proof-of-concept this already is good πŸ‘ – Xanthippe
N
0

Checking this issue, seems that what you are trying to accomplish can't be done with a Bindable Layout using a Grid as a Element.

The documentation isn't as clear as it should, nevertheless.

Nayarit answered 29/8, 2019 at 13:50 Comment(0)
M
0

You can subscribe to BindingContextChanged event and configure then all the items. You have to configure the grid definitions programatically after the event.

Marchioness answered 12/5, 2022 at 15:41 Comment(0)
R
0

This can be solved by property binding to Grid.Row and Grid.Column as well, e.g.

<Grid BindableLayout.ItemsSource="{Binding Items}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <Label Grid.Row="{Binding MyGridRowProperty}"
                   Grid.Column="{Binding MyGridColumnProperty}"
                   Text="{Binding MyProperty}"/>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</Grid>
Richela answered 18/6, 2023 at 21:50 Comment(4)
Which would require the bound class to explicitly contain properties for row and column, but then, yes. – Xanthippe
I mean that's the point about Grid right? You want to be able to specify where the components are going to appear right? If not, then, it kinda says Grid wasn't the component for you. You have FlexLayout+BindableLayout and let the system decide. Similarly with CollectionView, ListView and so forth. A clear use case would define one over another, e.g. I think Grid would be most appropriate if you're implementing a board game where each delegate actually needs to contain an X, Y for each piece. – Richela
That's the point of a Grid, yes, but not exactly the point of a BindableLayout, as already described in the other answers. In the fewest of cases one has a class that contains properties for row and column, and most of the time one does not even want those to be in the class that's used in the BindableLayout.ItemsSource. Long story short, what's actually needed is not possible with a Grid in Xamarin, and no, neither FlexLayout, nor CollectionView/ListView would suit that use-case. – Xanthippe
If you do not want them in the class, it is possible (1) to derive the index of any item from your item source, (2) to declare a ValueConverter that converts such index to appropriate Row or Column values for you on the fly. – Richela

© 2022 - 2025 β€” McMap. All rights reserved.