Search code examples
uicollectionviewmauiswipe

.net maui prevent swipe from triggering parent collectionview selected item


I'm new to .net maui and the question im asking might be obvious. I have a xaml with a swipeview inside a collectionview. I want to swipe and let the user choose one of the swipe items. However, after ~1 sec from swiping, the collectionview selecteditem/selected command are triggered and the user is sent to another page page instead of choosing a swipe item.

What am i missing/doing wrong? I'm following the mvvm pattern.

xaml is as follows (code behind just initializes the viewmodel):

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" 
             x:Class="GastoMisto.Pages.MateriaisGestaoLista"
             Title="Materiais">
    <ContentPage.ToolbarItems>
        <ToolbarItem Command="{Binding AdicionarMaterialCommand}" IconImageSource="Resources/Images/plus.svg" />
    </ContentPage.ToolbarItems>
    <ContentPage.Behaviors>
        <toolkit:EventToCommandBehavior EventName="Appearing" Command="{Binding LoadDataCommand}" />
    </ContentPage.Behaviors>
    <VerticalStackLayout Padding="5">
        <CollectionView ItemsSource="{Binding Materiais}"
                        SelectedItem="{Binding MaterialSelecionado, Mode=TwoWay}"
                        SelectionChangedCommand="{Binding MaterialSelectedCommand}"
                        SelectionMode="Single"
                        x:Name="cMateriaisView">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <SwipeView>
                        <SwipeView.LeftItems>
                            <SwipeItems Mode="Reveal">
                                <SwipeItem Text="Desativar" Command="{Binding DesativarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                                <SwipeItem Text="Apagar" Command="{Binding ApagarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                                <SwipeItem Text="Consultar" Command="{Binding ConsultarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                            </SwipeItems>
                        </SwipeView.LeftItems>

                        <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto" ColumnSpacing="5">
                            <Label Text="{Binding Codigo}" FontAttributes="Bold" Grid.Row="0" Grid.Column="0" />
                            <Label Text="{Binding Designacao}" Grid.Row="0" Grid.Column="1" />
                            <Label Text="{Binding Notas}" Grid.Row="1" Grid.ColumnSpan="2" />
                        </Grid>
                    </SwipeView>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</ContentPage>


The viewmodel code is as follows:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace GastoMisto.ViewModels
{
    public class MateriaisGestaoListaViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        private ObservableCollection<Model.Material> _materiais;
        public ObservableCollection<Model.Material> Materiais
        {
            get => _materiais;
            set => _materiais = value;
        }
      

        private  Model.Material _selectedPart;
        public Model.Material MaterialSelecionado
        {
            get => _selectedPart;
            set
            {
                if (_selectedPart == value)
                    return;

                _selectedPart = value;

                OnPropertyChanged();
            }
        }


        public ICommand DesativarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));
        public ICommand ApagarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));
        public ICommand ConsultarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));


        public ICommand LoadDataCommand => new Command(async () => await fillData()); 

        public ICommand MaterialSelectedCommand => new Command(async () => await SelecionaMaterial());


        public ICommand AdicionarMaterialCommand => new Command(async () => await Shell.Current.GoToAsync("Materiais/Ficha"));

        public MateriaisGestaoListaViewModel()
        {
            _materiais = new ObservableCollection<Model.Material>();         
            MessagingCenter.Subscribe<MateriaisGestaoFichaViewModel>(this, Messages.Refresh, async (sender) => await fillData());
            
        }

        private async Task fillData()
        {
            Materiais.Clear();
            var materiaisEmBd = await App.DbSingleton.Materiais.GetAll();

            foreach (var esteMaterial in materiaisEmBd)
            {
                Materiais.Add(esteMaterial);
            }
            
        }


        private async Task SelecionaMaterial()
        {

            if (MaterialSelecionado == null)
                return;

            var navigationParameter = new Dictionary<string, object>()
            {
                { "MatEmQuestao", MaterialSelecionado }
            };

            await Shell.Current.GoToAsync("Materiais/Ficha", navigationParameter);

            MainThread.BeginInvokeOnMainThread(() => MaterialSelecionado = null);
        }
    }
}

I have tried to use swiping events to enable/disable the itemselected/selected command to no avail. The maui sample for collection views with swipe doesn't do anything regarding the selected collection item. Should you add a SelectionChangedCommand="{Binding SomeCommand}" on it SomeCommand will be triggered as well.

I'm running this on an physical Android device with Android 10. The project is compiled for .net7.0.


Solution

  • It turns out my approach wasn't right and one must work around that .netmaui feature. To work around it i used a frame with tap gesture recognition instead of the collectionview's selecteditem/selecteditemcommand combination. In between changes I also introduced community mvvm to use the [RelayCommand] and strongly typed bindings.

    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                     xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" 
                     xmlns:viewmodel="clr-namespace:GastoMisto.ViewModels"
                     xmlns:model="clr-namespace:GastoMisto.Model"
                     x:Class="GastoMisto.Pages.MateriaisGestaoLista"   
                     Title="Materiais">
            <ContentPage.ToolbarItems>
                <ToolbarItem Command="{Binding AdicionarMaterialCommand}" IconImageSource="Resources/Images/plus.svg" />
            </ContentPage.ToolbarItems>
            <ContentPage.Behaviors>
                <toolkit:EventToCommandBehavior EventName="Appearing" Command="{Binding LoadDataCommand}" />
            </ContentPage.Behaviors>
            <VerticalStackLayout Padding="5">
                <CollectionView ItemsSource="{Binding Materiais}"
                                x:Name="cMateriaisView">
                    <CollectionView.ItemTemplate>
                        <DataTemplate>
                            <SwipeView>
                                <SwipeView.LeftItems>
                                    <SwipeItems Mode="Reveal">
                                        <SwipeItem Text="Desativar" Command="{Binding DesativarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                                        <SwipeItem Text="Apagar" Command="{Binding ApagarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                                        <SwipeItem Text="Consultar" Command="{Binding ConsultarCommand, Source={x:Reference Name=cMateriaisView}}" CommandParameter="{Binding .}" />
                                    </SwipeItems>
                                </SwipeView.LeftItems>
                                <Frame>
                                    <Frame.GestureRecognizers>
                                        <TapGestureRecognizer Command="{Binding Path=MaterialTappedCommand, Source={RelativeSource AncestorType={x:Type viewmodel:MateriaisGestaoListaViewModel}}}" 
                                                              CommandParameter="{Binding .}" />
                                    </Frame.GestureRecognizers>
                                    <Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto" ColumnSpacing="5">
                                        <Label Text="{Binding Codigo}" FontAttributes="Bold" Grid.Row="0" Grid.Column="0" />
                                        <Label Text="{Binding Designacao}" Grid.Row="0" Grid.Column="1" />
                                        <Label Text="{Binding Notas}" Grid.Row="1" Grid.ColumnSpan="2" />
                                    </Grid>
                                </Frame>
                            </SwipeView>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
            </VerticalStackLayout>
        </ContentPage>
    

    By using Path in the gesture along with relay command, the Path MaterialTappedCommand is relayed (due to autogenerated code) to the MaterialTapped method when tapped. The final viewmodel code is as follows:

    using CommunityToolkit.Mvvm.Input;
    using GastoMisto.Model;
    using IntelliJ.Lang.Annotations;
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    
    namespace GastoMisto.ViewModels
    {
        public partial class MateriaisGestaoListaViewModel : INotifyPropertyChanged
        {
            #region INotifyPropertyChanged
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            void OnPropertyChanged([CallerMemberName] string propertyName = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
    
            #endregion
    
            private ObservableCollection<Model.Material> _materiais;
            public ObservableCollection<Model.Material> Materiais
            {
                get => _materiais;
                set => _materiais = value;
            }
    
            [RelayCommand]
            async void MaterialTapped(Material material)
            {
                var navigationParameter = new Dictionary<string, object>()
                {
                    { "MatEmQuestao", material }
                };
    
                await Shell.Current.GoToAsync("Materiais/Ficha", navigationParameter);
            }
    
    
            public ICommand DesativarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));
            public ICommand ApagarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));
            public ICommand ConsultarCommand => new Command(async () => await Task.FromException<NotImplementedException>(new NotImplementedException()));
    
    
            public ICommand LoadDataCommand => new Command(async () => await fillData());
            public ICommand AdicionarMaterialCommand => new Command(async () => await Shell.Current.GoToAsync("Materiais/Ficha"));
    
    
    
            public MateriaisGestaoListaViewModel()
            {
                _materiais = new ObservableCollection<Model.Material>();         
                MessagingCenter.Subscribe<MateriaisGestaoFichaViewModel>(this, Messages.Refresh, async (sender) => await fillData());
                
            }
    
            private async Task fillData()
            {
                Materiais.Clear();
                var materiaisEmBd = await App.DbSingleton.Materiais.GetAll();
    
                foreach (var esteMaterial in materiaisEmBd)
                {
                    Materiais.Add(esteMaterial);
                }
                
            }
        }
    }
    

    Hopefully this will help someone else.