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

.NET MAUI binding ItemSelected event of ListView to ViewModel

提问人:Gavin Beard 提问时间:12/31/2022 最后编辑:JulianGavin Beard 更新时间:11/11/2023 访问量:4538

问:

我正在尝试将 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 中

C# .NET 事件 MVVM 毛伊

评论

0赞 Julian 12/31/2022
ItemSelected需要在视图的代码隐藏(.xaml.cs 文件)中定义的事件处理程序。在哪里定义?您正在尝试使用绑定,这不适用于此,事件处理程序通常仅存在于隐藏的代码中。应在 ViewModel 中使用 Commands。请展示您的代码,将其添加到问题中。顺便说一句,您应该制作属性的支持字段。OnItemTappedChangedprivate
0赞 Gavin Beard 12/31/2022
对不起,我在上面修改的代码中的小错误。OnItemTappedChanged 在视图模型中,我也尝试将其创建为 RelayCommand,但这给出了相同的错误
1赞 Jason 12/31/2022
ItemSelected是一个事件。不能将命令绑定到事件。不能将任何内容绑定到事件。可以用作解决此问题的解决方法,或者只让代码隐藏中的事件处理程序调用 VM 命令。EventToCommandBehavior

答:

7赞 Julian 12/31/2022 #1

ItemSelected需要一个事件处理程序,该处理程序通常只存在于视图的代码隐藏中。由于 ViewModel 应该对 View 一无所知,因此最好不要混合概念。您有几个选项可以在不破坏 MVVM 模式的情况下解决此问题。

选项 1:使用 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>

请注意,这使用绑定。

然后,您可以在 your 中定义一个将所选项目作为参数的方法:CategoryViewModel

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

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

选项 2:使用 EventToCommandBehavior

还可以使用 MAUI 社区工具包中的 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
    }

    // ...
}

评论

0赞 mfranc28 10/13/2023
它不起作用...项目为 null...。我认为您必须使用CommandParameter?
0赞 Julian 10/13/2023
@mfranc28 不知道你的意思。你期望在哪里?对于 ListView ,需要使用 ,而不是像您在官方文档中看到的那样CommandParameterSelectedItemEventArgsConverterCommandParameter
0赞 nikPast 11/8/2023 #2

朱利安的完美回答,它绝对有效。 请务必添加:

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

否则 Item 将为 null,如 mfranc28 突出显示的那样。