Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#

Construction and Design-Time Support of the RadioGroup User Control

Rate me:
Please Sign up or sign in to vote.
5.00/5 (15 votes)
22 Jul 2021CPOL12 min read 14.2K   486   11   8
How to create a .NET user control combining several radio buttons with a border and a caption and provide it with handy support of Visual Studio at design time.
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:

Image 1

We need to add some intelligence to this skeleton. First of all, add collections of names and buttons:

C#
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:

C#
for (int i = buttons.Count; i < names.Count; ++i )
{
    // Create new button.
    RadioButton radioButton = new RadioButton();
    // Set its properties and its event handler.
    radioButton.AutoSize = true;
    radioButton.TabStop = true;
    radioButton.Click += button_Click;
    radioButton.Text = names[i];
    // Add the button to the collection.
    buttons.Add(radioButton);
}

The code which removes the buttons is shorter:

C#
// The event handler must be disconnected before removing
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:

C#
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.

C#
[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.

C#
public class NameCollection : StringCollection
{
    // Event ItemsChanged signals that the collection of strings was changed,
    // provides additional information: if the collection needs to be sorted.
    public delegate void ItemsChangedEventHandler(object sender, ItemsChangedEventArgs e);
    public event ItemsChangedEventHandler ItemsChanged;
    private void OnItemsChanged(ItemsChangedEventArgs e)
    {
        if (ItemsChanged != null) ItemsChanged(this, e);
    }
    // We have to override the StringCollection's methods of manipulating with items
    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:

C#
[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.

C#
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:

C#
[Category("Behavior")]
[Description("Set or get zero based index of the selected radioButton.")]
[DefaultValue(0)]
public int IndexSelected
{
    get
    {
        return this.indexSelected;
    }
    // The setter acts in a different way if the control is initializing: 
    // validation of the value
    // is postponed to the ISupportInitialize.EndInit() call.
    // The buttons will be updated after the Items property set.
    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.

C#
private bool registered = false;
public override ISite Site
{
    get
    {
        return base.Site;
    }
    set
    {
        base.Site = value;
        if (!registered)
        {
            // Connect the handler to the ComponentChanged event
            RegisterChangeNotification();
            registered = true;
        }
    }
}

We have to supplement the automatically generated code of the class to disconnect the handler.

C#
protected override void Dispose(bool disposing)
{
    if (disposing && (components != null))
    {
        components.Dispose();
    }
    // Disconnect the handler from the ComponentChanged event
    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.

C#
public class RadioGroupDesigner : System.Windows.Forms.Design.ControlDesigner
{
    private IComponentChangeService changeService;
    public override void Initialize(IComponent component)
    {
        // The designer keeps reference to the designed control.
        base.Initialize(component);
        changeService = (IComponentChangeService)GetService(typeof(IComponentChangeService));
        // Connect the handler to the event
        if (changeService != null)
        {
            changeService.ComponentChanged +=
                new ComponentChangedEventHandler(ComponentChanged);
        }
    }
    protected override void Dispose(bool disposing)
    {
        // Disconnect the handler before disposing
        if (changeService != null)
        {
            changeService.ComponentChanged -=
                new ComponentChangedEventHandler(ComponentChanged);
        }
        base.Dispose(disposing);
    }
    private void ComponentChanged(object sender, ComponentChangedEventArgs e)
    {
        // We have to recognize the type of control and the name of changed property.
        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();
                // If the Items is changed then the buttons collection 
                // must be changed properly.
                radioGroup.UpdateButtons();
            }
        }
    }
}

Now the RadioGroup definition looks like this:

C#
[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.

Image 2

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.

C#
public class RadioGroupActionList : System.ComponentModel.Design.DesignerActionList
{
    private RadioGroup linkedControl;
    public RadioGroupActionList(RadioGroup control) : base(control)
    {
        this.linkedControl = control;
    }
    // A helper method to retrieve control properties.
    // GetProperties ensures undo and menu updates to work properly.
    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;
        }
    }
    // Properties that are targets of DesignerActionPropertyItem entries.
    public string Text
    {
        get
        {
            return linkedControl.Text;
        }
        set
        {
            GetPropertyByName("Text").SetValue(linkedControl, value);
        }
    }
....// Other properties are organized similarly
    public int ColumnCount
    {   ...   }
    public int IndexSelected
    {   ...   }
    public bool Sorted
    {   ...   }
    public RadioGroup.Direction FlowDirection
    {   ...   }
    // Method that is target of a DesignerActionMethodItem.
    // It calls the string collection editor.
    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));
    }
    // Implementation of this abstract method creates smart tag items,
    // associates their targets, and collects into list.
    public override DesignerActionItemCollection GetSortedActionItems()
    {
        DesignerActionItemCollection items = new DesignerActionItemCollection();
        // Begin by creating the headers.
        items.Add(new DesignerActionHeaderItem("Appearance"));
        items.Add(new DesignerActionHeaderItem("Data"));
        ... // other headers
        // Add items that wrap the properties.
        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]."));
        ... // other property items
        // Add an action link.
        items.Add(new DesignerActionMethodItem(this, "EditNames", 
                                               "Edit names collection...", "Data",
            "Opens the Lines collection editor", false));
        // Create entry for static Information section.
        items.Add(new DesignerActionTextItem(String.Format
                         ("Number of buttons: {0} (reopen to refresh)",
            linkedControl.Items.Count), "Information"));
        return items;
    }
    // The "magical code" from the MSDN we need to run a string collection editor.
    public class TypeDescriptionContext : ITypeDescriptorContext,
       IServiceProvider, IWindowsFormsEditorService
    {   /* See the uploaded project for details */   }
}

The last supplement of the RadioGroupDesigner is:

C#
public class RadioGroupDesigner : System.Windows.Forms.Design.ParentControlDesigner
{
    private IComponentChangeService changeService;
    ...
    // The designer infrastructure to create smart tags of the RadioGroup control
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer Ivan Franko National University of Lviv
Ukraine Ukraine
Head of Programming department in Ivan Franko National University of Lviv, UKRAINE

Comments and Discussions

 
GeneralMy vote of 5 Pin
DevJr19-Feb-22 23:35
DevJr19-Feb-22 23:35 
GeneralRe: My vote of 5 Pin
Сергій Ярошко19-Feb-22 23:52
professionalСергій Ярошко19-Feb-22 23:52 
QuestionConstruction and Design-Time Support of the RadioGroup User Control Pin
Jacky Joy25-Jul-21 3:08
Jacky Joy25-Jul-21 3:08 
AnswerRe: Construction and Design-Time Support of the RadioGroup User Control Pin
Сергій Ярошко19-Feb-22 23:50
professionalСергій Ярошко19-Feb-22 23:50 
PraiseRadioGroup User Control Pin
Member 1323841822-Jul-21 23:53
Member 1323841822-Jul-21 23:53 
GeneralRe: RadioGroup User Control Pin
Сергій Ярошко23-Jul-21 3:27
professionalСергій Ярошко23-Jul-21 3:27 
GeneralMy vote of 5 Pin
LightTempler22-Jul-21 9:32
LightTempler22-Jul-21 9:32 
GeneralRe: My vote of 5 Pin
Сергій Ярошко22-Jul-21 10:20
professionalСергій Ярошко22-Jul-21 10:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.