mvvmradio-buttonmauibindable

.Net Maui: How to group Radio buttons inside a BindableLayout?


I am working on a .Net Maui app where I have to use radio buttons to allow user to select an item from a list. This list is again a part of another list. The overall structure is like Item1(SubItem1,SubItem2,SubItem3,SubItem4), Item2((SubItem1,SubItem2,SubItem3,SubItem4), Item3((SubItem1,SubItem2,SubItem3,SubItem4), Item4((SubItem1,SubItem2,SubItem3,SubItem4)).

The itemsource (SubItem1,SubItem2,SubItem3,SubItem4) is same for all. I am using BindableLayout and ContentView to generate the view. I am sharing code issue reproduced in a sample app. My requirement is to select a single SubItem for an Item and its value should be stored in a bindable property SelectedItem.

The issue I am facing is whenever I select a subitem say SubItem1 for a Item1 then selected item in all the remaining list Item2,Item3,Item4 also get updated with SubItem1. I tried by making GroupName different for radio buttons and binding it. This is causing the issue that if I select one of the radio button then RadioButton_CheckedChanged event is called 4 times. I have to avoid mutiple calls here.

Can someone suggest where I am doing wrong?

Code for BindableLayout MainPage

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

Code Behind for MainPage

public partial class RadioButtonPage : ContentPage
{
    private RadioButtonViewModel viewModel { get; set; }
    public RadioButtonPage()
    {
    InitializeComponent();
        BindingContext = viewModel = new RadioButtonViewModel();
    }
}

ViewModel for MainPage

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

Models used:

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

Code for RadioButtonContentView:

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

Code behind for 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;
        }
    }
}

Solution

  • Based on the code you shared, I found you have missed property SelectedItem for ModelClass.cs while initializing the variable ModelItemList.

    So, I added such variable for ModelClass.cs, just as follows:

    public  class ModelClass: ObservableObject
    {
        private string _title = "";
        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }
    
        // add property SelectedItem here
        private PickerItem _selectedItem =new();
        public PickerItem SelectedItem
        {
            get => _selectedItem;
            set => SetProperty(ref _selectedItem, value);
        }
    
        private List<PickerItem> _itemList = new();
        public List<PickerItem> ItemList
        {
            get => _itemList;
            set => SetProperty(ref _itemList, value);
        }
    
        public ModelClass(string title, List<PickerItem> items, int selectedItemIndex)
        {
            this._title = title;
            this._itemList = items;
            this._selectedItem = _itemList[selectedItemIndex];
    
            //set selected item for each group 
            if (selectedItemIndex >=0 && selectedItemIndex < _itemList.Count)
            {
                ItemList[selectedItemIndex].IsSelected = true;
            }
        }
    }
    

    Besides,we also need to use the different list instance RadioButtonList for RadioButtonViewModel.cs, just as Nikos said. And the SelectedItem should be an element of the ItemList for each ModelClass instance. The way you used was not in the right way.

    You can refer to the following code:

     public partial class RadioButtonViewModel: ObservableObject
       {
           private List<ModelClass> _modelItemList = new();
           private object ItemList;
    
           public List<ModelClass> ModelItemList
           {
               get => _modelItemList;
               set => SetProperty(ref _modelItemList, value);
           }
    
    
           public RadioButtonViewModel()
           {
    
               ModelItemList = new()
           {
           new ModelClass ( "Item1",GetNewRadioButtonList() ,0),
           new ModelClass ("Item2", GetNewRadioButtonList() ,1),
           new ModelClass ("Item3", GetNewRadioButtonList(), 2),
           new ModelClass ("Item4", GetNewRadioButtonList(), 3)
    
           };
    
             List<PickerItem> GetNewRadioButtonList()
               {
                   return 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 }
          };
               }
           }
       }
    

    Note:

    I modified the way of setting the selectedItem, you can check the added constructor method of ModelClass:

      public ModelClass(string title, List<PickerItem> items, int selectedItemIndex)
        {
            this._title = title;
            this._itemList = items;
            this._selectedItem = _itemList[selectedItemIndex];
    
            //set selected item for each group 
            if (selectedItemIndex >=0 && selectedItemIndex < _itemList.Count)
            {
                ItemList[selectedItemIndex].IsSelected = true;
            }
        }