.NET Maui:如何在BindableLayout中正确分组RadioButton?
我正在开发一个.NET Maui应用,需要用RadioButton让用户从父项的子列表中选择项,结构是Item1(SubItem1-4)、Item2(SubItem1-4)等,所有父项的子项数据源相同。我用BindableLayout和ContentView生成视图,需求是每个父项仅能选一个子项,选中值存在绑定属性SelectedItem里。
目前遇到两个问题:
- 选择Item1的SubItem1时,Item2、Item3、Item4的选中项也同步变成SubItem1;
- 给RadioButton绑定不同GroupName后,
RadioButton_CheckedChanged事件会触发4次,需要避免多次调用。
主页面XAML代码
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MauiSampleApp.Views.RadioButtonPage" xmlns:controls="clr-namespace:MauiSampleApp.Controls" Title="RadioButtonPage"> <VerticalStackLayout Spacing="10"> <VerticalStackLayout BindableLayout.ItemsSource="{Binding ModelItemList}"> <BindableLayout.ItemTemplate> <DataTemplate> <Grid RowSpacing="6" RowDefinitions="20,auto"> <Label Grid.Row="0" Text="{Binding Title}"/> <controls:RadioButtonContentView Grid.Row="1" GroupName="{Binding Title}" ItemSource="{Binding ItemList}"/> </Grid> </DataTemplate> </BindableLayout.ItemTemplate> </VerticalStackLayout> </VerticalStackLayout> </ContentPage>
主页面后端代码
public partial class RadioButtonPage : ContentPage { private RadioButtonViewModel viewModel { get; set; } public RadioButtonPage() { InitializeComponent(); BindingContext = viewModel = new RadioButtonViewModel(); } }
主页面ViewModel代码
public class RadioButtonViewModel : ObservableObject { private List<ModelClass> _modelItemList = new(); public List<ModelClass> ModelItemList { get => _modelItemList; set => SetProperty(ref _modelItemList, value); } private PickerItem _selectedItem; public PickerItem SelectedItem { get => _selectedItem; set => SetProperty(ref _selectedItem, value); } public List<PickerItem> RadioButtonList { get; set; } public RadioButtonViewModel() { RadioButtonList = new() { new PickerItem { Key = "SubItem1", IsSelected = true }, new PickerItem { Key = "SubItem2", IsSelected = false }, new PickerItem { Key = "SubItem3", IsSelected = false }, new PickerItem { Key = "SubItem4", IsSelected = false } }; ModelItemList = new() { new ModelClass { Title = "Item1", ItemList = RadioButtonList, SelectedItem = RadioButtonList[0] }, new ModelClass { Title = "Item2", ItemList = RadioButtonList, SelectedItem = RadioButtonList[1] }, new ModelClass { Title = "Item3", ItemList = RadioButtonList, SelectedItem = RadioButtonList[2] }, new ModelClass { Title = "Item4", ItemList = RadioButtonList, SelectedItem = RadioButtonList[3] } }; } }
模型代码
ModelClass
public class ModelClass : ObservableObject { private string _title = ""; public string Title { get => _title; set => SetProperty(ref _title, value); } private List<PickerItem> _itemList = new(); public List<PickerItem> ItemList { get => _itemList; set => SetProperty(ref _itemList, value); } private PickerItem _selectedItem; public PickerItem SelectedItem { get => _selectedItem; set => SetProperty(ref _selectedItem, value); } }
PickerItem
public class PickerItem : ObservableObject { private string _key = ""; public string Key { get => _key; set => SetProperty(ref _key, value); } private bool _isSelected = false; public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected, value); } }
RadioButtonContentView XAML代码
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MauiSampleApp.Controls.RadioButtonContentView" x:Name="this"> <VerticalStackLayout BindableLayout.ItemsSource="{Binding ItemSource, Source={x:Reference this}}" RadioButtonGroup.GroupName="{Binding GroupName, Source={x:Reference this}}"> <BindableLayout.ItemTemplate> <DataTemplate> <Grid ColumnSpacing="12" ColumnDefinitions="20,*"> <RadioButton Grid.Column="0" IsChecked="{Binding IsSelected}" Value="{Binding .}" CheckedChanged="RadioButton_CheckedChanged"/> <Label Grid.Column="1" Text="{Binding Key}"/> <Grid.GestureRecognizers> <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" CommandParameter="{Binding .}"/> </Grid.GestureRecognizers> </Grid> </DataTemplate> </BindableLayout.ItemTemplate> </VerticalStackLayout> </ContentView>
RadioButtonContentView后端代码
public partial class RadioButtonContentView : ContentView { public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create( nameof(SelectedItem), typeof(PickerItem), typeof(RadioButtonContentView), null, propertyChanged: SelectedItemPropertyChanged); public PickerItem SelectedItem { get => (PickerItem)GetValue(SelectedItemProperty); set => SetValue(SelectedItemProperty, value); } private static void SelectedItemPropertyChanged(BindableObject bindable, object oldValue, object newValue) { if (oldValue is PickerItem oldPickerItem) { oldPickerItem.IsSelected = false; } if (newValue is PickerItem newPickerItem) { newPickerItem.IsSelected = true; } } public static readonly BindableProperty ItemSourceProperty = BindableProperty.Create( nameof(ItemSource), typeof(List<PickerItem>), typeof(RadioButtonContentView), new List<PickerItem>()); public List<PickerItem> ItemSource { get => (List<PickerItem>)GetValue(ItemSourceProperty); set => SetValue(ItemSourceProperty, value); } public static readonly BindableProperty GroupNameProperty = BindableProperty.Create( nameof(GroupName), typeof(string), typeof(RadioButtonContentView), ""); public string GroupName { get => (string)GetValue(GroupNameProperty); set => SetValue(GroupNameProperty, value); } public RadioButtonContentView() { InitializeComponent(); } void RadioButton_CheckedChanged(object sender, CheckedChangedEventArgs e) { if (sender is RadioButton radioButton) { if (e.Value) { var selectedPickerItem = radioButton.Value; SelectedItem = (PickerItem)selectedPickerItem; } } } void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e) { if (e.Parameter is PickerItem selectedPickerItem) { SelectedItem = selectedPickerItem; } } }
问题修复方案
1. 所有父项选中状态同步的问题
原因:ViewModel中所有ModelClass实例的ItemList都引用了同一个RadioButtonList集合,PickerItem是引用类型,修改任意一个父项中PickerItem的IsSelected属性,会影响所有引用该对象的父项。
修复:给每个ModelClass创建独立的PickerItem集合,避免复用同一列表。可以封装一个创建子列表的方法减少重复代码:
private List<PickerItem> CreatePickerItems(int defaultSelectedIndex) { var items = new List<PickerItem> { new PickerItem { Key = "SubItem1", IsSelected = false }, new PickerItem { Key = "SubItem2", IsSelected = false }, new PickerItem { Key = "SubItem3", IsSelected = false }, new PickerItem { Key = "SubItem4", IsSelected = false } }; if(defaultSelectedIndex >=0 && defaultSelectedIndex < items.Count) { items[defaultSelectedIndex].IsSelected = true; } return items; } // 在ViewModel构造函数中调用 public RadioButtonViewModel() { ModelItemList = new() { new ModelClass { Title = "Item1", ItemList = CreatePickerItems(0), SelectedItem = CreatePickerItems(0)[0] }, new ModelClass { Title = "Item2", ItemList = CreatePickerItems(1), SelectedItem = CreatePickerItems(1)[1] }, new ModelClass { Title = "Item3", ItemList = CreatePickerItems(2), SelectedItem = CreatePickerItems(2)[2] }, new ModelClass { Title = "Item4", ItemList = CreatePickerItems(3), SelectedItem = CreatePickerItems(3)[3] } }; }
2. CheckedChanged事件多次触发的问题
原因:设置SelectedItem时会修改PickerItem.IsSelected,而该属性绑定到RadioButton的IsChecked,会再次触发CheckedChanged事件;同时组内未选中的RadioButton状态变化也会触发事件。
修复:添加状态标记避免循环触发,只处理选中状态的事件:
public partial class RadioButtonContentView : ContentView { private bool _isUpdatingSelection = false; void RadioButton_CheckedChanged(object sender, CheckedChangedEventArgs e) { if (_isUpdatingSelection) return; if (sender is RadioButton radioButton && e.Value) { _isUpdatingSelection = true; SelectedItem = (PickerItem)radioButton.Value; _isUpdatingSelection = false; } } private static void SelectedItemPropertyChanged(BindableObject bindable, object oldValue, object newValue) { var view = (RadioButtonContentView)bindable; if (view._isUpdatingSelection) return; view._isUpdatingSelection = true; if (oldValue is PickerItem oldPickerItem) { oldPickerItem.IsSelected = false; } if (newValue is PickerItem newPickerItem) { newPickerItem.IsSelected = true; } view._isUpdatingSelection = false; } // 其余代码不变 }
同时,需要将RadioButtonContentView的SelectedItem与父项ModelClass的SelectedItem双向绑定,在主页面XAML中修改:
<controls:RadioButtonContentView Grid.Row="1" GroupName="{Binding Title}" ItemSource="{Binding ItemList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
这样每个父项的选中状态会独立存储在自身的SelectedItem属性中,不会互相干扰。
内容的提问来源于stack exchange,提问作者Swati Chandra
相关产品推荐
相关产品推荐

