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.