다음을 통해 공유


Working with RadioButtons Win Forms (C#)

Introduction

Learn how to bind a group of RadioButton controls to a strong typed list in Windows form projects. 

Prerequisites

  • Basic knowledge of C#
  • Basic understanding of generics.

Requires

  • Microsoft Visual Studio 2019

Basics

When there is a need for mutually exclusive selection from a selection of values radio buttons provide an easy way to get user's selections unlike a group of CheckBox controls where the a user can pick one or more selections. 

Example, business requirements indicate to show product categories with radio buttons.

An easy solution is create each control dynamically rather than hand place them on a form. For this example data is read from a text file (data source does not matter) then  each line becomes a radio button.

Note there is no exception handling for reading the text file although for a real application there should be both exception handling an assertion for proper types.

namespace RadioButtonBinding.Classes
{
    public class CreateRadioButtons
    {
        public List<RadioButton> RadioButtons { get; set; }
        public string RadioBaseName { get; set; }
        public static int Base { get; set; }
        public static int BaseAddition { get; set; }
        /// <summary>
        /// Parent control to place RadioButton controls on
        /// </summary>
        public static Control ParentControl { get; set; }
 
        /// <summary>
        /// Create one RadioButton for each category read from a comma delimited
        /// file. Could also change from reading a file to reading from a database
        /// table.
        /// </summary>
        public static void CreateCategoryRadioButtons()
        {
            var categories = DataOperations.ReadCategoriesFromCommaDelimitedFile();
 
            foreach (var category in categories)
            {
                RadioButton radioButton = new()
                {
                    Name = $"{category.Name}RadioButton",
                    Text = category.Name,
                    Location = new Point(5, Base),
                    Parent = ParentControl,
                    Tag = category.CategoryId,
                    Visible = true
                };
                 
                ParentControl.Controls.Add(radioButton);
                Base += BaseAddition;
            }
        }
    }
}

In the calling form each radio button subscribes to an anonymous CheckChanged event which could point to an event in the form.

private void  OnShown(object  sender, EventArgs e)
{
    /*
     * Indicate were to create the radio buttons
     */
    CreateRadioButtons.ParentControl = CategoryFlowLayoutPanel;
     
    /*
     * Create a radio button for each line in categories.csv
     */
    CreateRadioButtons.CreateCategoryRadioButtons();
     
    /*
     * Setup event to get current checked radio button which
     * permits using the primary key to say get products by category.
     */
    CategoryFlowLayoutPanel.RadioButtonList().ForEach(radioButton => 
        radioButton.CheckedChanged += ( _ , _ ) =>
        {
            if (radioButton?.Checked == true)
            {
                SelectedLabel.Text = $"{radioButton.Tag}, {radioButton.Text}";
            }
        } 
    );
}

To get the selection this extension method gets the selection if any.

public static  RadioButton RadioButtonChecked(this Control control, bool pChecked = true) =>
    control.Descendants<RadioButton>().ToList()
        .FirstOrDefault((radioButton) => radioButton.Checked == pChecked);

Binding a strong typed list

Working with list can prove difficult, let's make this easy. The task is to bind to a Person class where Gender and Suffix have multiple values and the task is to both show and allow changes at runtime.

public class  Person : INotifyPropertyChanged
{
    private string  _firstName;
    private string  _lastName;
    private GenderType _gender;
    private SuffixType _suffix;
    public int  Id { get; set; }
 
    public string  FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            OnPropertyChanged();
        }
    }
 
    public string  LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            OnPropertyChanged();
        }
    }
 
    public string  FullName => $"{FirstName} {LastName}";
 
    public GenderType Gender
    {
        get => _gender;
        set
        {
            _gender = value;
            OnPropertyChanged();
        }
    }
 
    public SuffixType Suffix
    {
        get => _suffix;
        set
        {
            _suffix = value;
            OnPropertyChanged();
        }
    }
 
    public string  LineArray()
    {
        return $"{Id},{FirstName},{LastName},{(int)Gender},{(int)Suffix}";
    }
    public event  PropertyChangedEventHandler PropertyChanged;
    protected virtual  void OnPropertyChanged([CallerMemberName]  string  propertyName = null)
    {
        PropertyChanged?.Invoke(this, new  PropertyChangedEventArgs(propertyName));
    }
}

Rather than coding for both gender and suffix, the following generic method allows any data source.

public static  class ControlHelpers
{
    public static  void RadioCheckedBinding<T>(RadioButton radio, object  dataSource, string  dataMember, T trueValue)
    {
        var binding = new  Binding(nameof(RadioButton.Checked), 
            dataSource, dataMember, true, DataSourceUpdateMode.OnPropertyChanged);
         
        binding.Parse += (s, args) =>
        {
            if ((bool)args.Value)
            {
                args.Value = trueValue;
            }
        };
         
        binding.Format += (s, args) => args.Value = ((T)args.Value).Equals(trueValue);
        radio.DataBindings.Add(binding);
    }
}

Like in the basic code sample, a text file is used to read from and can be replaced with reading from a database.

Note there is no exception handling for reading the text file although for a real application there should be both exception handling an assertion for proper types.

public static  List<Person> ReadPeopleFromCommaDelimitedFile() =>
    File.ReadAllLines(_personFileName).Select(content => content.Split(','))
        .Select(parts => new  Person()
        {
            Id = Convert.ToInt32(parts[0]),
            FirstName = parts[1],
            LastName = parts[2],
            Gender = EnumConverter<GenderType>.Convert(Convert.ToInt32(parts[3])),
            Suffix = EnumConverter<SuffixType>.Convert(Convert.ToInt32(parts[4]))
        })
        .ToList();

To convert both Gender and Suffix a converter is used to strong type the properties.

public static  class EnumConverter<TEnum> where TEnum :  struct, IConvertible
{
    public static  readonly Func<long, TEnum> Convert = GenerateConverter();
 
    public static  Func<long, TEnum> GenerateConverter()
    {
        var parameter = Expression.Parameter(typeof(long));
 
        var dynamicMethod = Expression
            .Lambda<Func<long, TEnum>>(
                Expression.Convert(parameter, typeof(TEnum)), parameter);
 
        return dynamicMethod.Compile();
    }
}

The calling for read the data into a BindingList<Person> which is assigned as the data source to a BindingSource. Both first and last name used conventional data binding.

private void  OnShown(object  sender, EventArgs e)
{
    _peopleBindingList = new  BindingList<Person>(DataOperations.ReadPeopleFromCommaDelimitedFile());
     
    _peopleBindingSource.DataSource = _peopleBindingList;
 
    /*
     * Provides navigation of people
     */
    PeopleNavigator.BindingSource = _peopleBindingSource;
 
    RadioCheckedBinding(MissRadioButton, _peopleBindingSource, "Suffix", SuffixType.Miss);
    RadioCheckedBinding(MrsRadioButton, _peopleBindingSource, "Suffix", SuffixType.Mrs);
    RadioCheckedBinding(MrRadioButton, _peopleBindingSource, "Suffix", SuffixType.Mr);
    RadioCheckedBinding(MaleRadioButton, _peopleBindingSource, "Gender", GenderType.Male);
    RadioCheckedBinding(FemaleRadioButton, _peopleBindingSource, "Gender", GenderType.Female);
    RadioCheckedBinding(OtherRadioButton, _peopleBindingSource, "Gender", GenderType.Other);
 
 
    FirstNameTextBox.DataBindings.Add("Text", _peopleBindingSource, "FirstName");
    LastNameTextBox.DataBindings.Add("Text", _peopleBindingSource, "LastName");
}

To save changes, in this case back to the original text file.

public static  void SaveAll(List<Person> peopleList)
{
    var sb = new  StringBuilder();
    foreach (var person in peopleList)
    {
        sb.AppendLine(person.LineArray());
    }
     
    File.WriteAllText(_personFileName, sb.ToString());
     
}

Although this may seem like a lot of code, the alternative is to write more code where each radio button will need the tag property set to identify the value for each control along with manually assigning the current person's gender and suffix as presented next.

public partial  class Version1Form : Form
{
    private readonly  BindingSource _peopleBindingSource = new();
    private BindingList<Person> _peopleBindingList;
 
    public Version1Form()
    {
        InitializeComponent();
 
        Shown += MainForm_Shown;
 
        /*
         * Used in RadioButtonGroupBox.Selected property.
         * Tag can also be set in the property window of
         * each RadioButton
         */
        MrRadioButton.Tag = 1;
        MrsRadioButton.Tag = 2;
        MissRadioButton.Tag = 3;
 
        FemaleRadioButton.Tag = 1;
        MaleRadioButton.Tag = 2;
        OtherRadioButton.Tag = 3;
 
    }
 
    private void  MainForm_Shown(object sender, EventArgs e)
    {
        /*
         * Setup events to update current person
         */
        SuffixRadioGroupBox.SelectedChanged += SuffixRadioGroupBox_SelectedChanged;
        GenderRadioGroupBox.SelectedChanged += GenderRadioGroupBox_SelectedChanged;
 
 
        /*
         * Setup data source from mocked data
         */
        _peopleBindingList = new  BindingList<Person>(DataOperations.ReadPeopleFromCommaDelimitedFile());
         
        _peopleBindingSource.DataSource = _peopleBindingList;
 
        /*
         * Setup data bindings to Suffix and Gender properties which are both enumerations, for
         * real applications these are int type.
         */
        SuffixRadioGroupBox.DataBindings.Add("Selected", _peopleBindingSource, "Suffix");
        GenderRadioGroupBox.DataBindings.Add("Selected", _peopleBindingSource, "Gender");
 
        /*
         * Setup data bindings for string properties
         */
        FirstNameTextBox.DataBindings.Add("Text", _peopleBindingSource, "FirstName");
        LastNameTextBox.DataBindings.Add("Text", _peopleBindingSource, "LastName");
 
        /*
         * Provides navigation of people
         */
        PeopleNavigator.BindingSource = _peopleBindingSource;
 
    }
 
    /// <summary>
    /// Set current person's gender type
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void  GenderRadioGroupBox_SelectedChanged(object sender, RadioGroupBox.SelectedChangedEventArgs e)
    {
        _peopleBindingList[_peopleBindingSource.Position].Gender = (GenderType)e.Selected;
    }
 
    /// <summary>
    /// Set current person's suffix type
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void  SuffixRadioGroupBox_SelectedChanged(object sender, RadioGroupBox.SelectedChangedEventArgs e)
    {
        _peopleBindingList[_peopleBindingSource.Position].Suffix = (SuffixType) e.Selected;
    }
 
    /// <summary>
    /// Display all people to ensure changes done are proper
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void  InspectButton_Click(object sender, EventArgs e)
    {
        var sb = new  StringBuilder();
        foreach (var person in _peopleBindingList)
        {
            sb.AppendLine($"{person.Suffix,4} {person.FullName} {person.Gender}");
        }
 
        MessageBox.Show(sb.ToString());
    }
 
    /// <summary>
    /// Save all people back to file
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void  SaveAllButton_Click(object sender, EventArgs e)
    {
 
        DataOperations.SaveAll(_peopleBindingList.ToList());
 
    }
 
}

Summary

By following along with this article working with radio buttons becomes an easy task.

References

Microsoft Windows Forms Data Binding 
Microsoft Binding Class 
DataGridView with ComboBox binding

External references

toggle-switch-winforms code sample

See also

Windows Forms overview
Windows forms data binding listbox/combobox
Working with ListViews

Source code

Source code can be cloned using Visual Studio or Git Desktop. Note there are several code sample in the repository not covered above to review.

https://github.com/karenpayneoregon/data-binding-win-forms