Home > Back-end >  .NET MAUI binding ItemSelected event of ListView to ViewModel
.NET MAUI binding ItemSelected event of ListView to ViewModel

Time:01-01

I am trying to bind the ItemSelected of a ListView to a View Model, but am experiencing some issues (due to my own misunderstands around how it all works).

I have view:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:Local="clr-namespace:FireLearn.ViewModels"
             x:Class="FireLearn.MainPage"
             Title="Categories">

    <ContentPage.BindingContext>
        <Local:CategoryViewModel/>
    </ContentPage.BindingContext>
    <NavigationPage.TitleView>
        <Label Text="Home"/>
    </NavigationPage.TitleView>
    <ListView
        ItemsSource="{Binding Categories}"
        HasUnevenRows="True"
        IsPullToRefreshEnabled="True"
        IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
        RefreshCommand="{Binding RefreshCommand}"
        ItemSelected="{Binding OnItemTappedChanged}"
        SelectionMode="Single"
        SelectedItem="{Binding SelectedCategory}">

        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <HorizontalStackLayout
                        Padding="8"
                        VerticalOptions="Fill"
                        HorizontalOptions="Fill">

                        <Image Source="cafs_bubbles.png"    
                               HeightRequest="64"
                               MaximumWidthRequest="64"
                               HorizontalOptions="CenterAndExpand"
                               VerticalOptions="CenterAndExpand"/>

                        <VerticalStackLayout
                            Padding="8"
                            VerticalOptions="FillAndExpand"
                            HorizontalOptions="FillAndExpand">
                            <Label Text="{Binding FormattedName}" 
                                       SemanticProperties.HeadingLevel="Level1"
                                       FontSize="Title"
                                       HorizontalOptions="Start"/>
                            <Label Text="{Binding ItemCount}" 
                                   FontSize="Subtitle"/>
                            <Label Text="{Binding Description}" 
                                   HorizontalOptions="Center"
                                   LineBreakMode="WordWrap"
                                   FontSize="Caption"
                                   VerticalOptions="CenterAndExpand"
                                   MaxLines="0"/>
                        </VerticalStackLayout>
                    </HorizontalStackLayout>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

This is linked to a view model:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FireLearn.Models;

namespace FireLearn.ViewModels
{
    public partial class CategoryViewModel : ObservableObject
    {
        public ObservableCollection<CategoryModel> categories = new ObservableCollection<CategoryModel>();

        public ObservableCollection<CategoryModel> Categories
        {
            get => categories;
            set => SetProperty(ref categories, value);
        }

        public bool listRefreshing = false;
        public bool ListRefreshing
        {
            get => listRefreshing;
            set => SetProperty(ref listRefreshing, value);
        }

        public CategoryModel selectedCategory = new CategoryModel();
        public CategoryModel SelectedCategory
        {
            get => selectedCategory;
            set
            {
                SetProperty(ref selectedCategory, value);
               // Tap(value);
            }
        }

        public RelayCommand RefreshCommand { get; set; }
        //public RelayCommand TapCellCommand { get; set; }

        public CategoryViewModel()
        {
            loadFromSource();
            RefreshCommand = new RelayCommand(async () =>
            {
                Debug.WriteLine($"STARTED::{ListRefreshing}");
                if (!ListRefreshing)
                {
                    ListRefreshing = true;
                    try
                    {
                        await loadFromSource();
                    }
                    finally
                    {
                        ListRefreshing = false;
                        Debug.WriteLine($"DONE::{ListRefreshing}");
                    }
                }
            });
        }

        public async Task loadFromSource()
        {
            HttpClient httpClient = new()
            {
                Timeout = new TimeSpan(0, 0, 10)
            };

            Uri uri = new Uri("https://somewebsite.co.uk/wp-json/wp/v2/categories");

            HttpResponseMessage msg = await httpClient.GetAsync(uri);

            if (msg.IsSuccessStatusCode)
            {
                var result = CategoryModel.FromJson(await msg.Content.ReadAsStringAsync());
                Categories = new ObservableCollection<CategoryModel>(result);
            }

            Debug.WriteLine("List Refreshed");
        }

        public void OnItemTappedChanged(System.Object sender, Microsoft.Maui.Controls.SelectedItemChangedEventArgs e)
        {
            var x = new ShellNavigationState();
            
            Shell.Current.GoToAsync(nameof(NewPage1),
                new Dictionary<string, object>
                {
                    {
                        nameof(NewPage1),
                        SelectedCategory
                    }
                });
        }
    }
}

I get compiler error "No property, BindableProperty, or event found for "ItemSelected", or mismatching type between value and property" and am really unsure of how to resolve. If I let XAML create a new event for me, it adds it in MainPage.Xaml.Cs rather than the VM

CodePudding user response:

ItemSelected expects an event handler which usually only exists in the View's code behind. Since the ViewModel shouldn't know anything about the View, it's better not to mix concepts. You have a couple of options to get around this without breaking the MVVM pattern.

Option 1: Use Event Handler and invoke method of ViewModel

First, set up the code behind with the ViewModel by passing it in via the constructor and also add the event handler, e.g.:

public partial class MainPage : ContentPage
{
    private CategoryViewModel _viewModel;

    public MainPage(CategoryViewModel viewModel)
    {
        _viewModel = viewModel;
    }

    public void OnItemSelectedChanged(object sender, SelectedItemChangedEventArgs e)
    {
        //call a method from the ViewModel, e.g.
        _viewModel.DoSomething(e.SelectedItem);
    }

    //...
}

Then use the event handler from within the XAML:

<ListView
    ItemsSource="{Binding Categories}"
    HasUnevenRows="True"
    IsPullToRefreshEnabled="True"
    IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
    RefreshCommand="{Binding RefreshCommand}"
    ItemSelected="OnItemSelectedChanged"
    SelectionMode="Single"
    SelectedItem="{Binding SelectedCategory}">

    <!-- skipping irrelevant stuff -->

</ListView>

Mind that this does not use bindings.

In your CategoryViewModel you could then define a method that takes in the selected item as an argument:

public partial class CategoryViewModel : ObservableObject
{
    //...

    public void DoSomething(object item)
    {
        //do something with the item, e.g. cast it to Category
    }
}

Option 2: Use EventToCommandBehavior

Instead of handling the invocation of a ViewModel method from your code behind, you could also use the EventToCommandBehavior from the MAUI Community Toolkit:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:Local="clr-namespace:FireLearn.ViewModels"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Class="FireLearn.MainPage"
             Title="Categories">

    <ContentPage.Resources>
         <ResourceDictionary>
             <toolkit:SelectedItemEventArgsConverter x:Key="SelectedItemEventArgsConverter" />
         </ResourceDictionary>
    </ContentPage.Resources>

    <ListView
        ItemsSource="{Binding Categories}"
        HasUnevenRows="True"
        IsPullToRefreshEnabled="True"
        IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
        RefreshCommand="{Binding RefreshCommand}"
        SelectionMode="Single"
        SelectedItem="{Binding SelectedCategory}">
        <ListView.Behaviors>
            <toolkit:EventToCommandBehavior
                EventName="ItemSelected"
                Command="{Binding ItemSelectedCommand}"
                EventArgsConverter="{StaticResource SelectedItemEventArgsConverter}" />
        </ListView.Behaviors>

        <!-- skipping irrelevant stuff -->

    </ListView>

</ContentPage>

Then, in your ViewModel, you can define the ItemSelectedCommand:

public partial class CategoryViewModel : ObservableObject
{
    [RelayCommand]
    private void ItemSelected(object item)
    {
        //do something with the item, e.g. cast it to Category
    }

    // ...
}

This is the preferred way to do it. Option 1 is just another possiblity, but the EventToCommandBehavior is the better choice.

Note that this is an example using MVVM Source Generators (since you're already using the MVVM Community Toolkit). The full Command would normally be implemented like this:

public partial class CategoryViewModel : ObservableObject
{
    private IRelayCommand<object> _itemSelectedCommand;
    public IRelayCommand<object> ItemSelectedCommand => _itemSelectedCommand ?? (_itemSelectedCommand = new RelayCommand<object>(ItemSelected));

    private void ItemSelected(object item)
    {
        //do something with the item, e.g. cast it to Category
    }

    // ...
}
  • Related