You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MAUI中通过CollectionView动态添加自定义控件报错求助

MAUI中CollectionView绑定自定义控件报错问题

我正尝试在MAUI中以编程方式向页面添加自定义控件。父页面有一个CollectionView,手动添加控件时渲染正常,但通过CollectionView绑定动态添加时出现不明确错误。


父页面CollectionView的XAML代码

<CollectionView Grid.Row="2" Grid.ColumnSpan="2" 
                SelectionMode="None"
                ItemsSource="{Binding Players}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="{x:Type contentViews:PlayerStubContentView}">
            <SwipeView>
                <SwipeView.RightItems>
                    <SwipeItems>
                        <SwipeItem Text="Delete"
                                   Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:PlayersViewModel}}, Path=DeleteCommand}"
                                   CommandParameter="{Binding .}"
                                   BackgroundColor="Red"/>
                    </SwipeItems>
                </SwipeView.RightItems>
                <Grid Padding="0.5">
                    <Frame>
                        <Frame.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:PlayersViewModel}}, Path=TapCommand}"
                                   CommandParameter="{Binding .}"/>
                        </Frame.GestureRecognizers>
                        <Label Text="{Binding .}"
                            FontSize="24"/>
                    </Frame>
                </Grid>
            </SwipeView>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

主页面ViewModel代码

加载方法

private async void LoadPlayerSave()
{
    var playerModels = await _playerService.GetPlayers();
    foreach(var playerModel in playerModels)
    {
        Players.Add(new PlayerStubContentView() { Name = playerModel.Name, PowerLevel = playerModel.LastPowerLevel.ToString(), Points = playerModel.Points.ToString()});
    }
}

可观察属性

[ObservableProperty]
ObservableCollection<PlayerStubContentView> players;

自定义控件PlayerStubContentView代码

XAML部分

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         x:Class="PodManager.ContentViews.PlayerStubContentView">
<ContentView.ControlTemplate>
    <ControlTemplate>
        <Grid ColumnDefinitions=".30*, auto, auto, auto"
      Padding="10"
      ColumnSpacing="10">
            <Label Text="{TemplateBinding Name}"
           Grid.Row="1"
           Grid.ColumnSpan="4"
           FontAttributes="Bold"/>
            <Label Text="Powerlevel:"
           Grid.Column="1"/>
            <Label Text="{TemplateBinding PowerLevel}"
           Grid.Column="2"/>
            <Label Text="Points:"
           Grid.Column="3"/>
            <Label Text="{TemplateBinding Points}"
           Grid.Column="4"/>
        </Grid>
    </ControlTemplate>
</ContentView.ControlTemplate>

代码后置部分

public partial class PlayerStubContentView : ContentView
{
public string Name
{
get => GetValue(NameProperty) as string;
set => SetValue(NameProperty, value);
}

public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(PlayerStubContentView));

public string PowerLevel
{
    get => GetValue(NameProperty) as string;
    set => SetValue(NameProperty, value);
}

public static readonly BindableProperty PowerLevelProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(PlayerStubContentView));

public string Points
{
    get => GetValue(NameProperty) as string;
    set => SetValue(NameProperty, value);
}

public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(PlayerStubContentView));

public PlayerStubContentView()
{
    InitializeComponent();
}
}

页面ViewModel命令代码

[RelayCommand]
async void AddNewPlayer()
{
    var newPlayerModel = new PlayerModel() { Name = NewPlayerName, LastPowerLevel = 5, Points = 0 };

    Players.Add(new PlayerStubContentView() { Name = newPlayerModel.Name});
    await _playerService.SavePlayer(newPlayerModel);
    NewPlayerName = string.Empty;
}

[RelayCommand]
async void Delete(string s)
{
    // TODO Figure this out with the new view model approach for the custom component
    //if (Players.Select(x => x.Name).Contains(s))
    //{
    //    var playerToDelete = Players.FirstOrDefault(x => x.Name == s);
    //    Players.Remove(playerToDelete);
    //    await _playerService.DeletePlayer(playerToDelete);
    //}
}

[RelayCommand]
async Task Tap(string s)
{
    await Shell.Current.GoToAsync($"{nameof(PlayerDetailPage)}?PlayerName={s}");
}

问题分析与修复方案

核心问题

  1. 自定义控件绑定属性错误:PowerLevel和Points属性的GetValue/SetValue都错误复用了NameProperty,且创建绑定属性时参数名也写错,导致所有属性都指向同一个绑定字段。
  2. CollectionView模板逻辑错误:模板中没有渲染自定义控件,而是用Label直接绑定控件实例(显示的是控件的字符串表示),而非控件本身。
  3. MVVM原则违背:ViewModel的集合存储的是View控件(PlayerStubContentView),而非数据模型(PlayerModel),导致绑定逻辑混乱。

修复步骤

1. 修正自定义控件的绑定属性

public partial class PlayerStubContentView : ContentView
{
    public string Name
    {
        get => GetValue(NameProperty) as string;
        set => SetValue(NameProperty, value);
    }

    public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(PlayerStubContentView));

    public string PowerLevel
    {
        get => GetValue(PowerLevelProperty) as string;
        set => SetValue(PowerLevelProperty, value);
    }

    public static readonly BindableProperty PowerLevelProperty = BindableProperty.Create(nameof(PowerLevel), typeof(string), typeof(PlayerStubContentView));

    public string Points
    {
        get => GetValue(PointsProperty) as string;
        set => SetValue(PointsProperty, value);
    }

    public static readonly BindableProperty PointsProperty = BindableProperty.Create(nameof(Points), typeof(string), typeof(PlayerStubContentView));

    public PlayerStubContentView()
    {
        InitializeComponent();
    }
}

2. 修正CollectionView的DataTemplate

模板中直接使用自定义控件,并绑定数据模型的属性:

<CollectionView Grid.Row="2" Grid.ColumnSpan="2" 
                SelectionMode="None"
                ItemsSource="{Binding Players}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="{x:Type models:PlayerModel}">
            <SwipeView>
                <SwipeView.RightItems>
                    <SwipeItems>
                        <SwipeItem Text="Delete"
                                   Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:PlayersViewModel}}, Path=DeleteCommand}"
                                   CommandParameter="{Binding .}"
                                   BackgroundColor="Red"/>
                    </SwipeItems>
                </SwipeView.RightItems>
                <Grid Padding="0.5">
                    <Frame>
                        <Frame.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:PlayersViewModel}}, Path=TapCommand}"
                                   CommandParameter="{Binding Name}"/>
                        </Frame.GestureRecognizers>
                        <!-- 渲染自定义控件并绑定数据 -->
                        <contentViews:PlayerStubContentView 
                            Name="{Binding Name}"
                            PowerLevel="{Binding LastPowerLevel.ToString()}"
                            Points="{Binding Points.ToString()}"/>
                    </Frame>
                </Grid>
            </SwipeView>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

3. 修正ViewModel的数据类型

将集合类型改为数据模型PlayerModel,遵循MVVM分离原则:

[ObservableProperty]
ObservableCollection<PlayerModel> players;

// 加载方法修改为添加数据模型
private async void LoadPlayerSave()
{
    var playerModels = await _playerService.GetPlayers();
    foreach(var playerModel in playerModels)
    {
        Players.Add(playerModel);
    }
}

// AddNewPlayer方法修改
[RelayCommand]
async void AddNewPlayer()
{
    var newPlayerModel = new PlayerModel() { Name = NewPlayerName, LastPowerLevel = 5, Points = 0 };

    Players.Add(newPlayerModel);
    await _playerService.SavePlayer(newPlayerModel);
    NewPlayerName = string.Empty;
}

// Delete命令修正为接收PlayerModel参数
[RelayCommand]
async void Delete(PlayerModel player)
{
    if (player != null && Players.Contains(player))
    {
        Players.Remove(player);
        await _playerService.DeletePlayer(player);
    }
}

4. 修正Tap命令参数

[RelayCommand]
async Task Tap(PlayerModel player)
{
    if (player != null)
    {
        await Shell.Current.GoToAsync($"{nameof(PlayerDetailPage)}?PlayerName={player.Name}");
    }
}

内容的提问来源于stack exchange,提问作者Delo1987

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 22:15:27