Our experience of using of RadioGroup control demonstrated some problem: it was impossible to change forecolor of a control's caption. We fix this bug and supplemented the control with new properties and methods.
Introduction
Let us suppose we want to ask a user to make a choice from some alternatives. This task is quite simple: we can put several radio buttons into the application window and recognize programmatically which of them the user has selected. If we want to have several separate groups of RadioButton
controls, the most common method is to group them in some container controls, such as Panel
or GroupBox
. It works well at first, but after a couple iterations with container and buttons, we will desire to have something more useful, for example, a control containing a collection of radio buttons.
Earlier, I have often used the TRadioGroup
component from VCL. It was clear and handy, therefore I decided to create in Visual Studio a similar user control - the RadioGroup
, which will display a group of radio buttons bounded by border with a caption text. The key property of the RadioGroup
control will be Items
- a collection of strings representing the buttons names. The control creates radio buttons automatically, according to the Items
content, like a ListBox
control does, arranges the buttons uniformly over the own surface in one or more columns. The RadioGroup
control notifies a program about the button index that the user has selected.
At first glance, there are no obstacles to achieve this goal. We can construct a user control with a GroupBox
(to provide the control with a Text
and a border), a TableLayoutPanel
(to arrange the buttons) and indexed collections of names and buttons. The difficulties occur when we try to ensure that our control behaves properly at design time and the RadioGroup
interacts well with Visual Studio. In different sources, I found several methods to overcome these difficulties and described them in this article.
Design of the RadioGroup
We start working with the new Windows Forms Control Library project. The next steps are:
- Put a
GroupBox
on the design surface of the control, set its property Text
to an empty string and Dock
property to Fill
. - Put a
TableLayoutPanel
in the GroupBox
, set its Dock
property to Fill
, remove all but one Row Styles from the Rows
collection, fill Columns
collection with a limited number (e.g. eight) of proportional Column Styles, set the ColumnCount
property to 1
, set the handle method to the DoubleClick
event.
The number of rows in the control will be unlimited and the Rows
collection will be filled in programmatically, so we do not need more than one item in the collection at design time. On the contrary, the number of columns will be bounded from 1 to 8, so it is rationally to create complete Columns
collection at design time and set an actual number of columns programmatically by the TableLayoutPanel.ColumnCount
property. The DoubleClick
event handler signals to the parent control that the event occurred.
In Visual Studio Designer, the RadioGroup
control looks quite simple. It is shown below:

We need to add some intelligence to this skeleton. First of all, add collections of names and buttons:
public partial class RadioGroup: UserControl
{
private StringCollection names = new StringCollection();
private List<RadioButton> buttons = new List<RadioButton>();
...
}
The StringCollection
class is used in the ListBox
control for similar purpose, so it will be good to use it in our control too. The creation of buttons looks like this:
for (int i = buttons.Count; i < names.Count; ++i )
{
RadioButton radioButton = new RadioButton();
radioButton.AutoSize = true;
radioButton.TabStop = true;
radioButton.Click += button_Click;
radioButton.Text = names[i];
buttons.Add(radioButton);
}
The code which removes the buttons is shorter:
for (int i = names.Count; i < buttons.Count; ++i)
buttons[i].Click -= button_Click;
buttons.RemoveRange(names.Count, buttons.Count - names.Count);
The Items
property gives public access to the names
collection:
public StringCollection Items
{
get
{
return this.names;
}
set
{
this.names = value;
UpdateButtons();
}
}
Unfortunately, at this point, we met a first problem. The StringCollection
provides a lot of modifying methods, such as Add
(aString
), Clear
(), RemoveAt
(position
) and so on. But execution of the Items.Add("Name")
do not cause any changes in the buttons
collection. We will describe how to fix the bug in the next section.
Programming the RadioGroup's Run Time Behavior
Properties
Every user control inherits the Text
property, that holds a text associated with the control. We have to override the property to map the corresponding property of the included group box and to apply the BrowsableAttribute
and DesignerSerializationVisibilityAttribute
to make the property visible in the Properties
window of Visual Studio and serializable to the program code generated by the designer.
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public override string Text
{
get
{
return theGrpBox.Text;
}
set
{
theGrpBox.Text = value;
}
}
We have to do some work to fix the problem with the Item
property. Every changes of the names
collection must reflect on the buttons
collection. The name
collection should signal about changes to the parent control. For this purpose, we can define a new class that derives from StringCollection
, extend it with an event and redefine every add-remove methods to signal the event.
public class NameCollection : StringCollection
{
public delegate void ItemsChangedEventHandler(object sender, ItemsChangedEventArgs e);
public event ItemsChangedEventHandler ItemsChanged;
private void OnItemsChanged(ItemsChangedEventArgs e)
{
if (ItemsChanged != null) ItemsChanged(this, e);
}
public new void Add(string item)
{
base.Add(item);
OnItemsChanged(new ItemsChangedEventArgs(true));
}
...
}
private NameCollection names = new NameCollection();
public RadioGroup()
{
InitializeComponent();
names.ItemsChanged += names_ItemsChanged;
}
The full text of the class you can see in the uploaded archive.
We decorate the Items
property with attributes to make it editable and serializable in a proper way:
[Category("Data")]
[Description("The names of radiobuttons in the group.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Editor("System.Windows.Forms.Design.ListControlStringCollectionEditor,
System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
typeof(System.Drawing.Design.UITypeEditor))]
public NameCollection Items
{ ... }
Now the Items
property works well at run time but at design time, a problem still occurs: the designer does not call the set
method of the property, so buttons
collection remains unchanged. The RadioGroupDesigner
will help us in the next section.
The IndexSelected
property sets or gets a zero based index of the selected radio button. The setter validates a new value, changes the Checked
property of the button and signals IndexChanged
event. It can throw an ArgumentOutOfRangeException
.
public int IndexSelected
{
get
{
return this.indexSelected;
}
set
{
if (this.indexSelected != value)
{
if (value < 0 || value >= this.names.Count)
{
throw new ArgumentOutOfRangeException("IndexSelected", String.Format(
"Invalid Argument \"{0}\" is out of the range
[0; {1}] of the buttons indexes",
value, this.names.Count - 1));
}
this.indexSelected = value;
this.buttons[indexSelected].Checked = true;
OnIndexChanged(new EventArgs());
}
}
}
I was surprised: the exception was thrown every time the RadioGroup
was initialized. It turned out that the designer always serializes the control properties in alphabetical order. The initializing process goes in the same order, therefore, the value of IndexSelected
property validates while the Items
property remains empty. We can’t change the order of serialization so we have to implement the ISupportInitialize
interface in the RadioGroup
class. For this purpose, we include to the class isInitializing
field and two methods. First of them is BeginInit()
, it only sets isInitializing
to true
. Second one, EndInit()
, makes final assignments and validations and sets isInitializing
to false
. The setters of all properties dependent on the initialization order check the isInitializing
value and work accordingly to the situation.
Now the IndexSelected
property looks like this:
[Category("Behavior")]
[Description("Set or get zero based index of the selected radioButton.")]
[DefaultValue(0)]
public int IndexSelected
{
get
{
return this.indexSelected;
}
set
{
if (this.indexSelected != value)
{
if (isInitializing)
this.indexSelected = value;
else
{
CompleteSetIndexSelected(value);
OnIndexChanged(new EventArgs());
}
}
}
}
The ColumnCount
property defines quantity of columns the group will be separated to. For example, a group of six buttons may occupy one column or three columns by two rows or one row by six columns dependently on the ColumnCount
value. The FlowDirection
property specifies the direction in which buttons are laid out: top to down or left to right. The effect can be seen if column count and row count of the group both are greater than one.
The set
methods of these properties take into account the initialization mode of the control in order to avoid multiple rearranging buttons in the table.
Events
The IndexChanged
event is a specific event of the RadioGroup
control. It is also the default event. It raises when a user clicks on an unchecked button or sets a new value to the IndexSelected
property.
Mouse events occur with the enclosed controls of the RadioGroup
: with a button or a table layout panel. Thus we have to take care to transfer the events to the parent control. So the button_Click()
method calls the RadioGroup.OnClick()
and the theTableLtPnl_DoubleClick()
method calls the RadioGroup. OnDoubleClick()
. How about Click
event on the table panel? If we define a handler to the Click
then the DoubleClick
event never rise, so we don’t do it.
To my mind, a RadioButton.Click
event demonstrates a strange behavior: if the button TabIndex
property is equal to zero and Checked
property is true
, the event raises every time the form has loaded. That means that the Click
handler is executed right at the start of the program. I decided to make the RadioGroup.Click
event unbrowsable to diminish this effect. The new Click
event has BrowsableAttribute(false)
and a user can set a handler only in the program, but not in the Properties
window.
ForeColor Property
Every Windows Forms control has the ambient property ForeColor
. Unfortunately, it does not affect the title of a group box, so there is no way to change the color of a RadioGroup
control's title. How could we fix this bug? If we want to set certain color of the title of a groupBox1
, we have to assign it directly to the groupBox1.ForeColor
property. Normally, this assignment will break link to the corresponding property of the parent control.
The first attempt to fix the bug was to override inherited property ForeColor
in RadioGroup class and to define the setter method of it as groupBox1.ForeColor = value;
. The default value of the ForeColor
is SystemColors.ControlText
. It turned out that the designer cannot process correctly the attribute [DefaultValue(typeof(SystemColors), "ControlText")]
and assigns the value to the property every time a RadioGroup
control has been loading. Therefore, a RadioGroup
gets its own ForeColor
property and loses an ambient one. That is a new bug.
It was finally decided to design two new properties: TitleForeColor
and ButtonsForeColor
to manage colors of a RadioGroup
's title
and radiobuttons
correspondingly. Inherited property ForeColor
becomes needless so we make it unbrowsable. Any assignment to these properties breaks the link with the ambient ForeColor
. To restore the link, one can use ResetForeColors
method that turns forecolors
to their default values.
Size of the Control
The standard GroupBox
control or TableLayoutPanel
control does not care about the size of child controls. A user can set the MinimumSize
property at design time to be sure that no any child control is clipped. It may be complicated if a parent control is created at run time. To simplify calculation of the minimum size of a RadioGroup
the method SetCorrectMinimumSize
was introduced.
Design Time Support of the Control
We have already applied attributes to some parts of our control. We did it for several reasons. Firstly, these attributes supply information for being used in the Properties
window. For example:
CategoryAttribute("")
sets the category under which the property (or the event) appears in the Properties
window. If a category with this name doesn’t exist, it is created. DescriptionAttribute("")
specifies the text description that will be displayed for the property (or the event) in the Object Browser and the Properties
window. If the attribute marks a control class, then it defines the text for the Toolbox
tip of the control. DefaultEventAttribute("")
is a class attribute. When the application programmer double-clicks on the control, Visual Studio automatically adds an event handler for the default event. DefaultPropertyAttribute("")
is a class attribute, defines a property that is highlighted in the Properties window by default if the control is selected for the first time.
Secondly, the attributes attach other design-time components to our control and configure how the properties are serialized. Such of them:
EditorAttribute(typeOfEditor, typeOfEditorBaseClass)
specifies the editor to use to change a property. DefaultValueAttribute(aConstValue)
describes the initial value that is used for this property by the control constructor. As long as the property matches the corresponding initial value, it’s not serialized.
Very important for us DesignerAttribute(typeOfDesigner)
is a control class attribute. It specifies the class used to implement design-time services for the component. We will describe the RadioGroupDesigner
in the next two sections.
Using of IComponentChangeService
Remember we have to teach our control to build buttons at design time. There are two ways to do this. Each of them uses a reference to the component change service, but in the different classes.
The first way is to override the Site
property in the RadioGroup
class. This override allows the control to register event handlers for IComponentChangeService
events at the time the control is sited, which happens only in design mode. The additional data field registered helps us to prevent multiple registration.
private bool registered = false;
public override ISite Site
{
get
{
return base.Site;
}
set
{
base.Site = value;
if (!registered)
{
RegisterChangeNotification();
registered = true;
}
}
}
We have to supplement the automatically generated code of the class to disconnect the handler.
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
ClearChangeNotification();
base.Dispose(disposing);
}
In my opinion, this approach defaces the clear structure of the RadioGroup
class. The second way is more intelligent. We can write a special designer for our control and make it responsible for assigning the handler to the IcomponentChangeService.ComponentChanged
event. This event raises every time any property of any control of the application has changed while the application is being designed, so we have to recognize the type of control and the name of its changed property.
public class RadioGroupDesigner : System.Windows.Forms.Design.ControlDesigner
{
private IComponentChangeService changeService;
public override void Initialize(IComponent component)
{
base.Initialize(component);
changeService = (IComponentChangeService)GetService(typeof(IComponentChangeService));
if (changeService != null)
{
changeService.ComponentChanged +=
new ComponentChangedEventHandler(ComponentChanged);
}
}
protected override void Dispose(bool disposing)
{
if (changeService != null)
{
changeService.ComponentChanged -=
new ComponentChangedEventHandler(ComponentChanged);
}
base.Dispose(disposing);
}
private void ComponentChanged(object sender, ComponentChangedEventArgs e)
{
IComponent comp = (IComponent)e.Component;
if (comp.Site.Component.GetType() == typeof(RadioGroup))
{
RadioGroup radioGroup = (RadioGroup)comp.Site.Component;
if (e.Member.Name == "Items")
{
if (radioGroup.Sorted) radioGroup.Items.Sort();
radioGroup.UpdateButtons();
}
}
}
}
Now the RadioGroup
definition looks like this:
[Description("Displays a bounded group of radioButtons")]
[Designer(typeof(RadioGroupDesigner))]
[DefaultEvent("IndexChanged")]
public partial class RadioGroup: UserControl, ISupportInitialize
{
...
}
Smart Tags
Let’s make the RadioGroup
control more similar to the branded controls and enrich it with the handful design time tool – with smart tags.
Smart tags are the pop-up windows that appear next to a control when you click on the tiny arrow in the right up corner. Smart tags are similar to menus because they both have a list of items. However, these items can be commands (which are rendered like hyperlinks), or other controls like check boxes, drop-down lists, and more. They can also include static descriptive text. In this way, a smart tag can act like a mini Properties
window.
The picture below shows the smart tag window of a RadioGroup
. It allows the developer to set the most important properties of a RadioGroup
. It contains three text boxes to edit Text
, ColumnCount
and IndexSelected
properties, a link to the string collection editor, a check box for the Sorted
property, a drop-down list to pick a FlowDirection
value and a static information about number of buttons in the group.

To create the smart tag, we have to override the RadioGroupDesigner.ActionList
property. It should return a collection of commands we want to support. The associated code of such collection trends to be complex, thus it’s a good idea to encapsulate the action list in a separate class. This custom class should derive from DesignerActionList
.
Our RadioGroupActionList
class defines counterparts of every property of RadioGroup
class included to the action list and a method for the link. Any RadioGroupActionList
property gets a value of the corresponding RadioGroup
property directly but sets a value to it by the PropertyDescriptor.SetValue()
method to notify about the change other parts of the designer infrastructure.
public class RadioGroupActionList : System.ComponentModel.Design.DesignerActionList
{
private RadioGroup linkedControl;
public RadioGroupActionList(RadioGroup control) : base(control)
{
this.linkedControl = control;
}
private PropertyDescriptor GetPropertyByName(String propertyName)
{
PropertyDescriptor property;
property = TypeDescriptor.GetProperties(linkedControl)[propertyName];
if (property == null)
{
throw new ArgumentException("Matching property not found.", propertyName);
}
else
{
return property;
}
}
public string Text
{
get
{
return linkedControl.Text;
}
set
{
GetPropertyByName("Text").SetValue(linkedControl, value);
}
}
....
public int ColumnCount
{ ... }
public int IndexSelected
{ ... }
public bool Sorted
{ ... }
public RadioGroup.Direction FlowDirection
{ ... }
public void EditNames()
{
PropertyDescriptor itemsPropertyDescriptor = GetPropertyByName("Items");
TypeDescriptionContext context = new TypeDescriptionContext(linkedControl,
itemsPropertyDescriptor);
UITypeEditor editor =
(UITypeEditor)itemsPropertyDescriptor.GetEditor(typeof(UITypeEditor));
itemsPropertyDescriptor.SetValue(linkedControl,
editor.EditValue(context, context, linkedControl.Items));
}
public override DesignerActionItemCollection GetSortedActionItems()
{
DesignerActionItemCollection items = new DesignerActionItemCollection();
items.Add(new DesignerActionHeaderItem("Appearance"));
items.Add(new DesignerActionHeaderItem("Data"));
...
items.Add(new DesignerActionPropertyItem("Text", "Caption text", "Appearance",
"Sets the text in the border."));
items.Add(new DesignerActionPropertyItem
("ColumnCount", "Number of columns", "Appearance",
"Sets quantity of columns the buttons will be separated to. Must be in [1; 8]."));
...
items.Add(new DesignerActionMethodItem(this, "EditNames",
"Edit names collection...", "Data",
"Opens the Lines collection editor", false));
items.Add(new DesignerActionTextItem(String.Format
("Number of buttons: {0} (reopen to refresh)",
linkedControl.Items.Count), "Information"));
return items;
}
public class TypeDescriptionContext : ITypeDescriptorContext,
IServiceProvider, IWindowsFormsEditorService
{ }
}
The last supplement of the RadioGroupDesigner
is:
public class RadioGroupDesigner : System.Windows.Forms.Design.ParentControlDesigner
{
private IComponentChangeService changeService;
...
private DesignerActionListCollection actionLists;
public override DesignerActionListCollection ActionLists
{
get
{
if (actionLists == null)
{
actionLists = new DesignerActionListCollection();
actionLists.Add(new RadioGroupActionList((RadioGroup)Control));
}
return actionLists;
}
}
}
History
- 10th September, 2017 – First release
- 22nd July, 2021 –
forecolors
properties and SetCorrectMinimumSize
method added
References
In this article, the following sources have been used:
- "Pro .NET 2.0 Windows Forms and Custom Controls in C#" by Matthew MacDonald
- "C#. Component development in MS Visual Studio" by Pavel Agurov (in Russian)
- MSDN materials
- "Beginning Microsoft® Visual C#® 2008" by Karli Watson, Christian Nagel, Jacob Hammer Pedersen, Jon D. Reid, Morgan Skinner, Eric White