Search code examples
xamarinmvvmxamarin.formsdata-binding

How can we bind a Grids Width, which is a list item to a Property in a generic view model in xamarin forms?


I have a GridViewCard which is used as a Data Template in multiple screens in my Xamarin forms app. And all those pages have a viewmodel which extends from a base view model. Will it be possible to have the width of the GridCard bind to a GridLength property in the base view model without any dependency on the page/list in which it is being used ?

GridViewCard:

<Grid xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms" xmlns:i18n="clr-namespace:ScholarApp.Assets.Extensions;assembly=ScholarApp" xmlns:controls="clr-namespace:ScholarApp.Assets.Controls" xmlns:Pancake="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView" xmlns:converters="clr-namespace:ScholarApp.Assets.Converters" x:Class="ScholarApp.Assets.Templates.GridViewCard" RowSpacing="10" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Padding="{OnIdiom Phone='20,10,0,20',Tablet='30,15,0,30'}">
    <Grid.RowDefinitions>
        <RowDefinition Height="{OnIdiom Phone='200',Tablet='280'}" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{"**this value should be dynamically bound to the page in which it is being used**" }" />
    </Grid.ColumnDefinitions>

</Grid>

Multiple use cases in multiple pages

code snippet from one page:

<StackLayout Style="{DynamicResource Default}" Padding="0,5,0,0" IsVisible="{Binding IsViewVisible}">
                <flv:FlowListView x:Name="ExploreGridView" IsVisible="{Binding IsGridLayout}" BackgroundColor="Transparent" Margin="{OnIdiom Phone='0,0,20,0',Tablet='0,0,30,0'}" FlowColumnCount="{OnIdiom Phone='2',Tablet='3'}" SeparatorVisibility="None" HasUnevenRows="true" FlowItemsSource="{Binding HandpickedList}" VerticalScrollBarVisibility="Never">
                    <flv:FlowListView.FlowColumnTemplate>
                        <DataTemplate>
                            <templates:GridViewCard />
                        </DataTemplate>
                    </flv:FlowListView.FlowColumnTemplate>
                </flv:FlowListView>
            </StackLayout>

code snippet from another page:

<ScrollView VerticalScrollBarVisibility="Never"
                HorizontalScrollBarVisibility="Never"
                Orientation="Horizontal">
        <StackLayout Padding="{DynamicResource ExploreStkPadding20}"
                     Orientation="Horizontal"
                     Spacing="{OnIdiom Phone='20',Tablet='30'}"
                     HeightRequest="{DynamicResource HandpickedHeight}"
                     BindableLayout.ItemsSource="{Binding ViewData}">
            <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <templates:GridViewCard />
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </StackLayout>
    </ScrollView>

Solution

  • To get this you will need a couple of things:

    First, add a name (x:Name) to the Grid in the GridViewCard XAML file. You will use it later.

    <Grid xmlns="http://xamarin.com/schemas/2014/forms"
          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"    
          x:Class="SQDictionary.GridViewCard"
          RowSpacing="10"
          x:Name="LeGrid"
          HorizontalOptions="FillAndExpand"
          BackgroundColor="BlueViolet"
          VerticalOptions="FillAndExpand"
          Padding="{OnIdiom Phone='20,10,0,20',Tablet='30,15,0,30'}">
    

    2: Add a BindableProperty in your GridViewCard class file GridViewCard.cs.

    public static readonly BindableProperty GridWidthProperty =
            BindableProperty.Create(nameof(GridWidth),
                                    typeof(GridLength),
                                    typeof(GridViewCard),
                                    default(GridLength),
                                    propertyChanged:OnGridLengthChanged);
    
    public GridLength GridWidth
    {
        get => (GridLength)GetValue(GridWidthProperty);
        set { SetValue(GridWidthProperty, value); }
    }
    
    private static void OnGridLengthChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is GridViewCard gridView && newValue is GridLength value)
        {
            var column0 = gridView.LeGrid.ColumnDefinitions?.ElementAtOrDefault(0);
            if (column0 != null)
            {
                column0.Width = value;
            }
        }
    }
    

    This code will expose the GridLength property in the UserControl and also update the Grid Column definition (the Width) when the property changes.

    This code only sets the Width of the first column (as you indicated will have only one). But if you want to add more columns and use the same value you just need to iterate over the ColumnDefinition collection.

    3: Whenever you use the GridViewCard control you will be able to access the property we just created GridWidth and set a value.

    <DataTemplate>
        <templates:GridViewCard GridWidth="130" />
    </DataTemplate>
    

    But you have indicated you want to Bind the value from the ViewModel.

    Since you are using this custom control inside a Template and the value is coming from the ViewModel (as a property, not part of the collection bound to the "List" you will need to do a little trick.

    Let's add the property to the BaseViewModel.

    private double _gridColumnLength;
    public double GridColumnLength
    {
        get => _gridColumnLength;
        set
        {
           //Don't know how you are handling the **Set** and/or Raising of property changes.
          // Modify accordingly (if required)
            _gridColumnLength = value;
            RaisePropertyChanged(nameof(GridColumnLength));
        }
    }
    

    Now, using the FlowList as an example:

    You will use the name (x:Name) from the FlowList to access the BindingContext then the value of the Property.

    <flv:FlowListView x:Name="ExploreGridView" 
                    IsVisible="{Binding IsGridLayout}" 
                    BackgroundColor="Transparent" 
                    Margin="{OnIdiom Phone='0,0,20,0',Tablet='0,0,30,0'}" 
                    FlowColumnCount="{OnIdiom Phone='2',Tablet='3'}" 
                    SeparatorVisibility="None"                 
                    HasUnevenRows="true" 
                    FlowItemsSource="{Binding HandpickedList}" 
                    VerticalScrollBarVisibility="Never">
        <flv:FlowListView.FlowColumnTemplate>
            <DataTemplate>
                <templates:GridViewCard GridWidth="{Binding BindingContext.GridColumnLength, Source={x:Reference ExploreGridView}}" />
            </DataTemplate>
        </flv:FlowListView.FlowColumnTemplate>
    </flv:FlowListView>
    

    Now when setting your value on the GridColumnLength property of the ViewModels that have access to it will trigger the change up to the Grid.Column.Width on the GridViewCard custom control.

    Hope this helps.-