将.NET MAUI中ListView的ItemSelected事件绑定到ViewModel。

3

我正在尝试将 ListView 的 ItemSelected 绑定到视图模型,但遇到了一些问题(由于我自己对其工作方式的误解)。

我有一个视图:

<?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>

这与视图模型相关联:

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
                    }
                });
        }
    }
}

我得到了编译器错误:“未找到“ItemSelected”的属性、BindableProperty或事件,或值与属性之间的类型不匹配”,我真的不确定该如何解决。 如果让XAML为我创建一个新的事件,它会在MainPage.xaml.cs中添加,而不是VM。

ItemSelected 期望在视图的代码后台(*.xaml.cs 文件*)中定义一个事件处理程序。 OnItemTappedChanged 定义在哪里?您正在尝试使用绑定,但对于此操作无效,事件处理程序通常仅存在于代码后台中。您应该在 ViewModel 中使用 Commands。请展示您的代码后台,并将其添加到问题中。顺便说一下,您应该将属性的支持字段设置为“private”。 - Julian
抱歉,我的代码中有一个小错误,我已经在上面进行了更正。OnItemTappedChanged在视图模型中,我还尝试将其创建为RelayCommand,但是出现了相同的错误。 - Gavin Beard
1
ItemSelected 是一个事件。你不能将命令绑定到事件上。你也不能将任何东西绑定到事件上。你可以使用 EventToCommandBehavior 作为解决方法,或者只需在代码后台中拥有事件处理程序来调用 VM 命令。 - Jason
2个回答

7

ItemSelected期望事件处理程序,通常仅存在于视图的代码后面。由于ViewModel不应知道任何关于View的信息,最好不要混合概念。您有几个选项可以绕过此问题,而不会破坏MVVM模式

选项1:使用Event Handler并调用ViewModel方法

首先,通过构造函数将ViewModel传递给代码后面,并添加事件处理程序,例如:

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);
    }

    //...
}

然后在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>

请注意,这里不使用绑定。
在您的CategoryViewModel中,您可以定义一个以所选项作为参数的方法:
public partial class CategoryViewModel : ObservableObject
{
    //...

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

选项二:使用 EventToCommandBehavior

您可以使用 MAUI Community Toolkit 中的 EventToCommandBehavior,而不是从代码后台处理 ViewModel 方法的调用:

<?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>

然后,在您的ViewModel中,您可以定义ItemSelectedCommand

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

    // ...
}

这是首选的方式。选项1只是另一种可能性,但EventToCommandBehavior是更好的选择。

请注意,这是使用MVVM源生成器的示例(因为您已经在使用MVVM社区工具包)。完整的Command通常会像这样实现:

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
    }

    // ...
}

这是一个很好的解释,感谢您花时间详细说明各种选项。 - Gavin Beard
它不起作用...项目为空....我觉得你得使用CommandParameter? - undefined
@mfranc28 不太清楚你的意思。你希望在哪里使用 CommandParameter 属性?对于 ListView ,需要使用 SelectedItemEventArgsConverter 而不是 CommandParameter,你可以在官方文档中看到相关说明。 - undefined

0
完美的回答来自朱利安,它绝对有效。 确保添加:
<ContentPage.Resources>
         <ResourceDictionary>
             <toolkit:SelectedItemEventArgsConverter x:Key="SelectedItemEventArgsConverter" />
         </ResourceDictionary>
</ContentPage.Resources>

在页面的开头,否则项目将为空,正如mfranc28所指出的那样。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接