'Xamarin.Forms SwipeView CommandParameter is null unless you reload UI HotReload

Ok, so I have a CollectionView bound to a collection of 'Show' objects. There's a fair bit going on with this CollectionView. I have a command for when a user selects a show (single tap redirects to detail page), multiple item selecting to do multi item delete, and swiping to do basic publishing functions to each individual show. Everything works EXCEPT the swiping. For some reason, when one of the swipe commands is fired, the 'Show' object being passed to the command is null. HOWEVER, I have HotReload enabled for this, and if all I do is save the xaml file, it works fine. The passed through show is not null and it works as I would expect. When I save the xaml file, I don't even have to change anything, I literally just save with no new changes (ctrl + s), and the code starts working and my swipe item commands are fine. Here are my code references:

xaml:

<ContentPage.ToolbarItems>
        <ToolbarItem IconImageSource="icon_plus.png" Command="{Binding AddShowCommand}" />
        <ToolbarItem Text="Delete" IconImageSource="icon_trash.png" Command="{Binding DeleteMultipleShowsCommand}" CommandParameter="{Binding SelectedShows}" />
    </ContentPage.ToolbarItems>

    <!--
      x:DataType enables compiled bindings for better performance and compile time validation of binding expressions.
      https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/data-binding/compiled-bindings
    -->
    <RefreshView x:DataType="local:ShowsViewModel" Command="{Binding LoadShowsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
        <CollectionView x:Name="ItemsListView"
                ItemsSource="{Binding Shows}"
                SelectionMode="Multiple"
                SelectedItems="{Binding SelectedShows}"
                SelectionChangedCommand="{Binding SelectionChangedCommand}">
            <CollectionView.ItemTemplate>
                <DataTemplate>

                    <SwipeView>
                        <SwipeView.RightItems>
                            <SwipeItems>
                                <SwipeItem Text="Activate"
                                    IconImageSource="favorite.png"
                                    BackgroundColor="LightGreen"
                                    Command="{Binding Source={RelativeSource AncestorType={x:Type local:ShowsViewModel}}, Path=SwipeIsActiveCommand}"
                                    CommandParameter="{Binding}" />
                                <SwipeItem Text="Published"
                                    IconImageSource="delete.png"
                                    BackgroundColor="LightPink"
                                    Command="{Binding Source={RelativeSource AncestorType={x:Type local:ShowsViewModel}}, Path=SwipeIsPublishCommand}"
                                    CommandParameter="{Binding}" />
                                <SwipeItem Text="Sold Out"
                                    IconImageSource="delete.png"
                                    BackgroundColor="LightPink"
                                    Command="{Binding Source={RelativeSource AncestorType={x:Type local:ShowsViewModel}}, Path=SwipeIsSoldOutCommand}"
                                    CommandParameter="{Binding}" />
                            </SwipeItems>
                        </SwipeView.RightItems>
                        <!-- Content -->
                        <Grid BackgroundColor="White">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>

                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="1"/>
                            </Grid.RowDefinitions>

                            <StackLayout Grid.Column="0" Grid.Row="0" Padding="15" x:DataType="model:Show">
                                <Label Text="{Binding ShowTitle}" 
                                   Grid.Row="0"
                                   LineBreakMode="NoWrap" 
                                   Style="{DynamicResource ListItemTextStyle}" 
                                   FontSize="16" />
                                <Label Text="{Binding ShowDate}" 
                                   Grid.Row="1"
                                   LineBreakMode="NoWrap" 
                                   Style="{DynamicResource ListItemTextStyle}" 
                                   FontSize="16" />
                                <StackLayout.GestureRecognizers>
                                    <TapGestureRecognizer 
                                        NumberOfTapsRequired="1"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type local:ShowsViewModel}}, Path=ShowSelectedCommand}"        
                                        CommandParameter="{Binding}">
                                    </TapGestureRecognizer>
                                </StackLayout.GestureRecognizers>
                            </StackLayout>

                            <BoxView HeightRequest="1"
                               BackgroundColor="Black"
                               Grid.ColumnSpan="2"
                               Grid.Row="1"
                               VerticalOptions="End"/>
                        </Grid>
                        
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup Name="CommonStates">
                                <VisualState Name="Normal" />
                                <VisualState Name="Selected">
                                    <VisualState.Setters>
                                        <Setter Property="BackgroundColor" Value="Yellow" />
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </SwipeView>

                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </RefreshView>

xaml.cs

public partial class ShowsPage : ContentPage
    {
        private ShowsViewModel _viewModel;

        public ShowsPage()
        {
            InitializeComponent();
            BindingContext = _viewModel = new ShowsViewModel();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            _viewModel.OnAppearing();
        }
    }

ViewModel:

public class ShowsViewModel : BaseViewModel
    {
        public ObservableCollection<Show> Shows { get; }
        public ObservableCollection<object> SelectedShows { get; }

        public Command SelectionChangedCommand { get; }
        public Command ShowSelectedCommand { get; }
        public Command DeleteMultipleShowsCommand { get; }
        public Command AddShowCommand { get; }
        public Command LoadShowsCommand { get; }
        public Command SwipeIsPublishCommand { get; }
        public Command SwipeIsActiveCommand { get; }
        public Command SwipeIsSoldOutCommand { get; }

        public ShowsViewModel()
        {
            Title = "Shows";
            Shows = new ObservableCollection<Show>();
            SelectedShows = new ObservableCollection<object>();

            LoadShowsCommand = new Command(async () => await LoadItems());
            AddShowCommand = new Command(OnAddItem);
            ShowSelectedCommand = new Command<Show>(OnItemSelected);
            DeleteMultipleShowsCommand = new Command(async () => await DeleteShows(), () => CanDeleteShows());
            SelectionChangedCommand = new Command(DeleteMultipleShowsCommand.ChangeCanExecute);

            SwipeIsPublishCommand = new Command<Show>(async (show) => await PublishShow(show));
            SwipeIsActiveCommand = new Command<Show>(async (show) => await ActivateShow(show));
            SwipeIsSoldOutCommand = new Command<Show>(async (show) => await SoldOutShow(show));
        }

        private async Task LoadItems()
        {
            IsBusy = true;

            try
            {
                Shows.Clear();
                var shows = await DataService.Shows.GetItemsAsync();
                foreach (var show in shows)
                {
                    this.Shows.Add(show);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        public void OnAppearing()
        {
            IsBusy = true;
        }

        private async void OnAddItem(object obj)
        {
            await Shell.Current.GoToAsync(nameof(CreateShowPage));
        }

        private async void OnItemSelected(Show show)
        {
            if (show == null)
                return;

            // This will push the ItemDetailPage onto the navigation stack
            await Shell.Current.GoToAsync($"{nameof(ShowDetailPage)}?{nameof(ShowDetailViewModel.ShowId)}={show.ShowId}");
        }

        private bool CanDeleteShows()
        {
            return SelectedShows.Count() > 0;
        }

        private async Task DeleteShows()
        {
            if (SelectedShows != null && SelectedShows.Count() > 0)
            {
                bool confirm = await App.Current.MainPage.DisplayAlert("Delete?", $"Are you sure you want to delete the selected show(s)?", "Yes", "No");
                if (confirm)
                {
                    IsBusy = true;

                    var totalDeleted = 0;
                    foreach (var showobj in SelectedShows)
                    {
                        var show = (Show)showobj;
                        var response = await DataService.Shows.DeleteItemAsync(show.ShowId.ToString());
                        if (response)
                        {
                            totalDeleted++;
                        }
                    }

                    await App.Current.MainPage.DisplayAlert("Delete Results", $"Deleted {totalDeleted} show(s).", "Ok");

                    // This will reload the page
                    IsBusy = true;
                }
            }
        }

        private async Task PublishShow(Show show)
        {
            if (show != null)
            {
                show.IsPublished = !show.IsPublished;
                await DataService.Shows.UpdateItemAsync(show);

                var message = show.IsPublished ? "published" : "unpublished";
                await App.Current.MainPage.DisplayAlert($"Show Updated!", $"Show: {show.ShowTitle} has been {message}", "Ok");
            }
        }

        private async Task ActivateShow(Show show)
        {
            if (show != null)
            {
                show.IsActive = !show.IsActive;
                await DataService.Shows.UpdateItemAsync(show);

                var message = show.IsActive ? "activated" : "archived";
                await App.Current.MainPage.DisplayAlert($"Show Updated!", $"Show: {show.ShowTitle} has been {message}", "Ok");
            }
        }

        private async Task SoldOutShow(Show show)
        {
            if (show != null)
            {
                show.IsSoldOut = !show.IsSoldOut;
                await DataService.Shows.UpdateItemAsync(show);

                var message = show.IsSoldOut ? "sold out" : "not sold out";
                await App.Current.MainPage.DisplayAlert($"Show Updated!", $"Show: {show.ShowTitle} has been marked as {message}", "Ok");
            }
        }
    }

I'm testing using android physical device with USB debugging. I'm not sure why this is happening like this, and it seems weird that merely reloading the UI with HotReload would change anything.



Solution 1:[1]

Try:

CommandParameter="{Binding Source={RelativeSource Self}, Path=BindingContext}"

Previously I used the SwipeItem Invoked event as a work-around:

private async void PublishedSwipeItem_Invoked(object sender, System.EventArgs e)
{
  var show = (sender as SwipeItem)?.BindingContext as Show;

  if (show!= null)
    await viewModel.PublishShow(show);
}

Solution 2:[2]

I just discovered after quite a lot of searching:
I was having the same issue with the CommandParameter="{Binding .}" passing null to my delete command. I removed the compiled bindings x:DataType="" from the xml page and it worked. There must be more to that than I understand but hopefully this saves someone else some time.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Yunnosch