0

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);
}
QuanDar
  • 1,255
  • 2
  • 12
  • 20
  • Why not put the command on your `SpecificationDetailDbViewModel` and just pass the item you wish to remove to that? –  Dec 17 '20 at 17:03
  • I don't understand, why do you need "0" as a parameter? And didn't see the logic of ```SpecsConverter```. – Shaw Dec 18 '20 at 00:26
  • Have you tried to use the `TapGestureRecognizer` to get to the index? – Wendy Zang - MSFT Dec 18 '20 at 07:25
  • @Knoop, That would be great. How do I know at which index a user could add the specification detail on click? I can delete an item just by looping over the specification groups until I find the specification detail. But I can't find a solution for this: if a user wants to add a specs detail at a specific spec group. How would I know where to add the specs detail inside the correct specs group without an Index? – QuanDar Dec 18 '20 at 08:56
  • @Shaw, I use the converter so that I don't have to add Index to my ViewModel. I don't want to use "0" as a parameter. I need to pass the Index there. – QuanDar Dec 18 '20 at 08:57
  • @WendyZang-MSFT I did not try that. I can't find anything on Google about this. If you know a tutorial, I will check that out. Thanks. – QuanDar Dec 18 '20 at 08:58
  • @QuanDar You could use `StackLayout.GestureRecognizers`. If you still have issue for this, you could provide a code snippet with whole model for me to reproduce. – Wendy Zang - MSFT Dec 18 '20 at 09:19

2 Answers2

1
  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.

Shaw
  • 907
  • 1
  • 8
  • 20
  • (Personally, i think the method name is a bit loooong.) – Shaw Dec 18 '20 at 09:59
  • Yes, I agree, I posted the answer. But I find it super weird not being able to pass the index. The implementation would be much easier and faster performance. There would be so many use cases where an Index would come handy. It makes me mad not being able to do something so simple while I can do it with so many other frameworks :) – QuanDar Dec 18 '20 at 10:09
  • Alright, a bit late for me. Normally after getting the object, I use object.ID instead of manipulating the index, sometimes I need to sort or filter, the index is not so reliable. But you can also get index by "FindIndex" anyway. – Shaw Dec 18 '20 at 11:00
0

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; }
QuanDar
  • 1,255
  • 2
  • 12
  • 20
  • This is exactly what I had in mind in my comment! Good that you got it working. Imo with the index it's just a way to solve deletes, no reason to force the use if it isn't easy to do. If I have time this evening I might take a look though at my index solution with the nested lists because I think that should work as well (though in this case I would prefer this solution anyway) –  Dec 18 '20 at 11:28