How to bind index of item in nested BindableLayout.ItemsSource to CommandParameter - Xamarin

1.4k views Asked by At

I'm trying to figure out how you can get the index from a list inside XAML.

Context

A product has multiple specification categories/groups which contain the specification details.

  • Product & IngrediĆ«nten are the specification groups.
  • Land van afkomst : Nederland are the specs details

enter image description here

In the XAML code, I'm using a nested list. The application needs to pass the Index so the users can delete and add specifications correctly.

The index at Binding Source="0" /> & CommandParameter="0" needs to be passed instead of "0".

<StackLayout BindableLayout.ItemsSource="{Binding Product.Specifications}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <StackLayout>
                <Entry Text="{Binding Title, Mode=TwoWay}" />
                <StackLayout BindableLayout.ItemsSource="{Binding SpecificationDetails}">
                    <BindableLayout.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width=".44*" />
                                    <ColumnDefinition Width=".44*" />
                                    <ColumnDefinition Width=".12*" />
                                </Grid.ColumnDefinitions>
                                <Entry Grid.Row="0"
                                       Grid.Column="0"
                                       Text="{Binding Title}"
                                       Style="{StaticResource spec-entry-style}" />
                                <Entry Grid.Row="0"
                                       Grid.Column="1"
                                       Text="{Binding Description}"
                                       Style="{StaticResource spec-entry-style}" />
                                <!-- Delete specification detail -->
                                <Button Grid.Column="2"
                                        Text="X"
                                        Style="{StaticResource cancel-button-style}"
                                        Command="{Binding Path=BindingContext.DeleteSpecificationEntryCommand, Source={x:Reference Page}}">
                                    <Button.CommandParameter>
                                        <MultiBinding Converter="{StaticResource SpecsConverter}">
                                            <Binding Source="0" />
                                            <Binding Path="." />
                                        </MultiBinding>
                                    </Button.CommandParameter>
                                </Button>
                            </Grid>
                        </DataTemplate>
                    </BindableLayout.ItemTemplate>
                </StackLayout>
                <!-- Add specification detail -->
                <Button Text="Voeg specificatie toe"
                        Command="{Binding AddSpecicifationEntriesCommand}"
                        CommandParameter="0"
                        HorizontalOptions="Start" />
            </StackLayout>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</StackLayout>

<!-- Add Specification group -->
<Button Text="Voeg nieuwe specificatie toe"
        Command="{Binding AddNewSpecificationGroupCommand}" />

The specs group model:

public class SpecificationDbViewModel : INotifyPropertyChanged
{
    private int _id;
    private string _title;
    private ObservableCollection<SpecificationDetailDbViewModel> _specificationDetails;

    public int Id
    {
        get => _id;
        set 
        { 
            _id = value;
            RaisePropertyChanged(nameof(Id));
        }
    }

    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            RaisePropertyChanged(nameof(Title));
        }
    }

    public ObservableCollection<SpecificationDetailDbViewModel> SpecificationDetails
    {
        get => _specificationDetails;
        set
        {
            _specificationDetails = value;
            RaisePropertyChanged(nameof(Title));
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

The specs detail model:

public class SpecificationDetailDbViewModel : INotifyPropertyChanged
{
    private int _id;
    private string _title;
    private string _description;

    public int Id
    {
        get => _id;
        set
        {
            _id = value;
            RaisePropertyChanged(nameof(Id));
        }
    }

    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            RaisePropertyChanged(nameof(Title));
        }
    }
    public string Description
    {
        get => _description;
        set
        {
            _description = value;
            RaisePropertyChanged(nameof(Description));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

I'm using a MultiBinder converter to pass multiple values to the command. 1 method inside the ViewModel that removes the specifications:

private void ExecuteDeleteSpecificationEntryCommand(SpecificationDetailWithIndex specificationDetailWithIndex)
{
    Product.Specifications[specificationDetailWithIndex.Index].SpecificationDetails.Remove(specificationDetailWithIndex.SpecificationDetailDbViewModel);
}
2

There are 2 answers

3
Shaw On BEST ANSWER
  1. Don't bother your index, just can send THE object back as a parameter of the command.
Command="{...}"  //same binding
CommandParameter="{Binding .}"  //new line
  1. And define your command with the correct parameter in your ViewModel.
    public ICommand<SpecificationDetailDbViewModel> DeleteSpecificationEntryCommand => new Command<SpecificationDetailDbViewModel>(ExecuteDeleteSpecificationEntryCommand);

    private void ExecuteDeleteSpecificationDetailEntryCommand(SpecificationDetailDbViewModel item)
    {
        //remvoe item from collection
        Product.Specifications?.Remove(item);
    }

And you can also use groups in the list view btw.

1
QuanDar On

Since it seems very hard to get an Index inside the XAML I created a different solution. I still hope someone knows how to get Index inside nested stacklayout lists. My teacher says it is bad practice to loop over things when not needed for performance.

    <StackLayout BindableLayout.ItemsSource="{Binding Product.Specifications}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <StackLayout>
                <Entry Text="{Binding Title, Mode=TwoWay}" />
                <StackLayout BindableLayout.ItemsSource="{Binding SpecificationDetails}">
                    <BindableLayout.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width=".44*" />
                                    <ColumnDefinition Width=".44*" />
                                    <ColumnDefinition Width=".12*" />
                                </Grid.ColumnDefinitions>
                                <Entry Grid.Row="0"
                                       Grid.Column="0"
                                       Text="{Binding Title}"
                                       Style="{StaticResource spec-entry-style}" />
                                <Entry Grid.Row="0"
                                       Grid.Column="1"
                                       Text="{Binding Description}"
                                       Style="{StaticResource spec-entry-style}" />
                                <!-- Delete specification detail -->
                                <Button Grid.Column="2"
                                        Text="X"
                                        Style="{StaticResource cancel-button-style}"
                                        Command="{Binding Path=BindingContext.DeleteSpecificationDetailEntryCommand, Source={x:Reference Page}}" 
                                        CommandParameter="{Binding .}"/>
                            </Grid>
                        </DataTemplate>
                    </BindableLayout.ItemTemplate>
                </StackLayout>
                <!-- Add specification detail -->
                <Button Text="Voeg specificatie toe"
                        Command="{Binding Path=BindingContext.AddSpecicifationDetailEntryCommand, Source={x:Reference Page}}"
                        CommandParameter="{Binding .}" 
                        HorizontalOptions="Start" />
                <Button Text="{Binding Title, StringFormat='Verwijder {0}'}"
                                        Style="{StaticResource cancel-button-style}"
                                        FontSize="12"
                                        Command="{Binding Path=BindingContext.DeleteSpecificationGroupEntryCommand, Source={x:Reference Page}}"
                                        CommandParameter="{Binding .}"
                                        HorizontalOptions="Start" />
            </StackLayout>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</StackLayout>

<!-- Add Specification group -->
<Button Text="Voeg nieuwe specificatie toe"
        Command="{Binding AddSpecicifationGroupEntriesCommand}" />

Methods for adding and removing specs

 private void ExecuteAddSpecicifationGroupEntriesCommand()
    {
        Product.Specifications.Add(new SpecificationDbViewModel()
        {
            SpecificationDetails = new ObservableCollection<SpecificationDetailDbViewModel>()
                    {
                        new SpecificationDetailDbViewModel()
                    }
        });
    }
    

    private void ExecuteDeleteSpecicifationGroupEntriesCommand(SpecificationDbViewModel specsViewModel)
    {
        Product.Specifications.Remove(specsViewModel);
    }

    private void ExecuteAddSpecicifationDetailEntryCommand(SpecificationDbViewModel specsViewModel)
    {
        Product.Specifications[Product.Specifications.IndexOf(specsViewModel)].SpecificationDetails.Add(new SpecificationDetailDbViewModel());
    }

    private void ExecuteDeleteSpecificationDetailEntryCommand(SpecificationDetailDbViewModel specsDetailViewModel)
    {
        for(int i = 0; i < Product.Specifications.Count; i++)
        {
            if(Product.Specifications[i].SpecificationDetails.IndexOf(specsDetailViewModel) != -1)
            {
                Product.Specifications[i].SpecificationDetails.Remove(specsDetailViewModel);
                return;
            }
        }
    }

Create the commands inside the constructor of your viewmodel

        AddSpecicifationDetailEntryCommand = new Command<SpecificationDbViewModel>((SpecificationDbViewModel specificationDbViewModel) => ExecuteAddSpecicifationDetailEntryCommand(specificationDbViewModel));
        DeleteSpecificationDetailEntryCommand = new Command<SpecificationDetailDbViewModel>((SpecificationDetailDbViewModel specsDetailViewModel) => ExecuteDeleteSpecificationDetailEntryCommand(specsDetailViewModel));
        AddSpecicifationGroupEntriesCommand = new Command(() => ExecuteAddSpecicifationGroupEntriesCommand());
        DeleteSpecificationGroupEntryCommand = new Command<SpecificationDbViewModel>((SpecificationDbViewModel specificationDbViewModel) => ExecuteDeleteSpecicifationGroupEntriesCommand(specificationDbViewModel));

Properties

    public ICommand AddSpecicifationDetailEntryCommand { get; }
    public ICommand DeleteSpecificationDetailEntryCommand { get; }
    public ICommand AddSpecicifationGroupEntriesCommand { get; }
    public ICommand DeleteSpecificationGroupEntryCommand { get; }