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}"); }
问题分析与修复方案
核心问题
- 自定义控件绑定属性错误:
PowerLevel和Points属性的GetValue/SetValue都错误复用了NameProperty,且创建绑定属性时参数名也写错,导致所有属性都指向同一个绑定字段。 - CollectionView模板逻辑错误:模板中没有渲染自定义控件,而是用Label直接绑定控件实例(显示的是控件的字符串表示),而非控件本身。
- 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
相关产品推荐
相关产品推荐

