Combining Multiple Controls into One
Microsoft Developer Network
Summary: Demonstrates how to create a new control by combining multiple Microsoft Windows Forms controls together; one of a series of Windows control development samples to be read in conjunction with the associated overview article. (10 printed pages)
This article is the third in a five-article series on developing controls in Microsoft® .NET:
- Developing Custom Windows Controls Using Visual Basic .NET (overview)
- Adding Regular Expression Validation
- Combining Multiple Controls into One
- Extending the TreeView Control
- Drawing Your Own Controls Using GDI+
In this article, I will cover two key control development concepts: creating custom controls by combining several existing controls together, and supporting data binding to your custom control. For an example, I will be building a survey button control (see Figure 1), which is not the most frequently used control, but one that requires combining several other controls and includes a custom property that you may wish to bind to a data source.
Figure 1. The survey button control allows a group of radio buttons to be positioned and coded against as a single item.
Instead of using five individual radio buttons for each of these survey questions, these buttons could be combined into a single control that could be used and data-bound as a unit. This requires creating a control that is very similar to the "classic" Microsoft® ActiveX® control for Microsoft Visual Basic® 5.0 and Visual Basic 6.0, combining several standard controls into a new custom control, and is handled in Microsoft .NET through the creation of a user control. User controls are custom controls that inherit from the System.Windows.Forms.UserControl class (see the Microsoft Visual Studio .NET® documentation on Visual Basic and Microsoft Visual C#® concepts for more information) and have a design surface that is very similar to a Microsoft Windows® Form. Templates are provided to create this control in Visual Basic .NET or Microsoft Visual C#® .NET, so adding a new user control to a project is as easy as right-clicking your project, clicking Add, and then clicking Add User Control as shown in Figure 2.
Note If you have Visual Basic .NET Standard Edition or Visual C# .NET Standard Edition, as opposed to one of the versions of Visual Studio .NET (Professional, Enterprise Developer, or Enterprise Architect), then you will not have the UserControl template. Just download the code for this article and start with the finished version, or if you want to create a new empty user control, just create a Windows Form and change the line "Inherits System.Windows.Forms.Form" to "Inherits System.Windows.Forms.UserControl". You will get an error on one line of the new control, "Me.AutoScaleBaseSize = ...", but you can just remove the offending line and everything else should work.
Figure 2. You can add a new user control by right-clicking your project in Solution Explorer, or by using the Project menu on the main menu bar.
As just mentioned, a user control provides a design surface similar to a Windows Form, giving you a container onto which you can place other controls. Creating a composite control (a custom control that is made up of a combination of other controls) involves three main steps:
- Adding the constituent controls, positioning them, and configuring their properties as desired.
- Writing the code to make these controls work together as desired for your new component, along with whatever other code is required to make your control work.
- Creating the properties, methods, and events that describe your new component's public interface to programmers that are using it.
Following those steps, the first thing I did was to add five radio buttons to a new user control and position them as desired (see Figure 3). I removed the caption text from all the buttons, made them as small as possible in height and width, and then positioned them in a row.
Figure 3. Positioning the radio buttons
Note Using the control alignment options on the layout toolbar makes arranging these controls very easy: Just select them all, click Align tops to get them vertically aligned, and then remove/increase horizontal spacing until you achieve the look you want.
Once the controls are all sized and placed, I will change the names of the five radio buttons to rb0-rb4—names that were chosen for no reason other than to reduce the clutter in the control's code. This is a good time to make sure that the control names agree with their positioning (rb0-rb1-rb2-rb3-rb4) and with their tab order (to show the current values, on the from the View menu, click Tab Order, as shown in Figure 4)
Figure 4. Tab order can be viewed and set visually in Visual Studio .NET.
On a control like this, with multiple constituent controls in fixed positions, you either will want to size your control correctly once and then prevent the user from resizing it, or automatically adjust your layout as the control is resized. In this case, I want this control to have a fixed size; resizing radio buttons is not something I want to consider, so I have to add some code to my control to prevent the user from resizing it on the user's Form. By overriding the SetBoundsCore method, I can intercept all attempts to resize or move my control, and prevent any change other than moving the control. In my overridden SetBoundCore, I pass along the position information (x,y) unchanged, letting the control be moved as desired, but regardless of what height and width information is passed into the method, I always pass along a fixed size:
Const fixedWidth As Integer = 120 Const fixedHeight As Integer = 24 Protected Overrides Sub SetBoundsCore(ByVal x As Integer, ByVal y As Integer, _ ByVal [width] As Integer, ByVal height As Integer, _ ByVal specified As BoundsSpecified) MyBase.SetBoundsCore(x, y, fixedWidth, fixedHeight, specified) End Sub
If you wish to handle resizing in another way, such as adjusting the size and position of your constituent controls, then you should add your own code to the Resize event of your base class (UserControl):
Private Sub SurveyRadioButtons_Resize(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Resize
Only two things need to be exposed from this control to make it usable: a property representing the currently selected radio button and an event that fires whenever the selection is changed. To facilitate working with the multiple radio buttons, the first bit of code I will add to this control will place them into an array:
Const numButtons As Integer = 5 Dim radioButtons(numButtons - 1) As RadioButton Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() 'Add any initialization after the InitializeComponent() call radioButtons(0) = Me.rb0 radioButtons(1) = Me.rb1 radioButtons(2) = Me.rb2 radioButtons(3) = Me.rb3 radioButtons(4) = Me.rb4 Me.rb0.Checked = True End Sub
Next, I will add a property for the currently selected radio button:
Dim currentValue As Integer = 1 <System.ComponentModel.Category("Appearance")> _ Public Property CurrentSelection() As Integer Get Return currentValue End Get Set(ByVal Value As Integer) If Value > 0 And Value <= numButtons Then radioButtons(Value - 1).Checked = True End If End Set End Property
For this property to return the correct value, the variable currentValue must be kept -to-date with the currently selected radio button. The easiest way to accomplish this is to provide an event handler for the CheckedChanged event of all the radio buttons and update the value in this handler:
Public Event CurrentSelectionChanged As EventHandler Const numButtons As Integer = 5 Dim radioButtons(numButtons - 1) As RadioButton Dim currentValue As Integer = 1 Private Sub rb_CheckedChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles rb0.CheckedChanged, rb1.CheckedChanged, _ rb2.CheckedChanged, rb3.CheckedChanged, _ rb4.CheckedChanged Dim rb As RadioButton = CType(sender, RadioButton) If rb.Checked Then Dim i As Integer = 0 Dim buttonFound As Boolean = False Do While (i < numButtons) And Not buttonFound If radioButtons(i).Checked Then currentValue = i + 1 buttonFound = True RaiseEvent CurrentSelectionChanged(CObj(Me), e) End If i += 1 Loop End If End Sub
This event handler will get called twice whenever the value changes—once when the previously selected radio button is unchecked, and once when the new radio button is checked—so the code uses "If rb.Checked" to ensure that it only processes the event once. Once the new value has been determined, the code raises an event to announce that the value has changed.
Now that my new SurveyRadioButtons control is working, I can move on to looking at the appearance of the control and making sure that it correctly supports data binding.
When the user has selected a standard radio button, it is indicated by the use of a focus rectangle (a dotted rectangle around the control). That rectangle is only drawn around the text of the radio button, not the entire control, and therefore when the radio button is used without any text (as is the case in this survey control), there is no visible indicator that the control has focus. That is not really acceptable, as the user wouldn't know which control was selected until actually changing the value of the survey button. Therefore, I want to add my own focus rectangle that is displayed around my entire custom control. Luckily for me, it is a relatively easy task because the .NET Framework includes a special class called the System.Windows.Forms.ControlPaint class to handle common control drawing tasks. This class exposes a variety of shared (static in C#) methods, including the DrawFocusRectangle method, which draws a standard Windows focus rectangle at the specific location you specify. Using that method, and a private Boolean flag that I set in the GotFocus and LostFocus events, I overrode the OnPaint method of my parent class (UserControl) and added a focus rectangle to my survey buttons.
Note After reading through the available properties of a control, I would have used the ContainsFocus property (which returns True if the control, or any one of its constituent controls, has the focus) instead of my hasFocus variable, but ContainsFocus calls the underlying Microsoft Win32® APIs, which can be a bit of a performance hit. Since my OnPaint routine will be called often, I do not want to be causing calls to the Win32 API when a little bit of extra code allows me to replace that call with a variable:
Dim hasFocus As Boolean = False Dim showFocusRect As Boolean = MyBase.ShowFocusCues Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs) hasFocus = True End Sub Protected Overrides Sub OnLostFocus(ByVal e As System.EventArgs) hasFocus = False End Sub Private Sub SurveyRadioButtons_ChangeUICues(ByVal sender As Object, _ ByVal e As System.Windows.Forms.UICuesEventArgs) _ Handles MyBase.ChangeUICues showFocusRect = e.ShowFocus End Sub Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim focusRect As Rectangle = Me.ClientRectangle focusRect.Inflate(-2, -2) If hasFocus And showFocusRect Then 'Draw focus rectangle around control ControlPaint.DrawFocusRectangle(e.Graphics, _ focusRect, Me.ForeColor, Me.BackColor) Else 'erase focus rectangle e.Graphics.DrawRectangle(New Pen(Me.BackColor, 1), focusRect) End If MyBase.OnPaint(e) End Sub Protected Overrides Sub OnEnter(ByVal e As System.EventArgs) Me.Invalidate() 'Invalidate to force redraw of focus rectangle End Sub Protected Overrides Sub OnLeave(ByVal e As System.EventArgs) Me.Invalidate() 'Invalidate to force redraw of focus rectangle End Sub
Since the focus rectangle will only be drawn or erased when the control is painted, the calls to Invalidate in the OnEnter and OnLeave are required to force a redraw whenever the control gains or loses focus.
Visual Basic developers have been able to link UI elements, such as controls, directly to data sources (a process known as data binding) for several versions now, and Visual Basic .NET is no exception. There are two types of data binding, sometimes described as simple and complex, and each type is implemented differently. Simple binding is done on a property level, where a field from your data source is linked to a single property of one of your controls (the Text property of a TextBox control, for instance). Complex binding doesn't bind at the property level; it is for binding an entire data source to your control. To help you understand the difference between these two types of binding, consider some common examples of each: binding the field "firstname" from a table to a TextBox, so that the TextBox displays the value of that field, is simple binding, while binding an entire list of authors (with multiple columns) to a DataGrid control is complex binding.
Since data binding is a common technique for developers, you will want to make sure that any control you create correctly supports it; but in .NET, for simple data binding at least, this is so easy it is hard to get wrong. You actually do not have to do anything to support simple data binding; by default, any of your control's properties can be bound to a data source and they will automatically be linked correctly. There is no real restriction on which properties can be bound, allowing you to data-bind a control's Tab index property if you wish, but often there are only a few properties that you will ever wish to bind. To make life easier on developers, the data-binding settings on a few select properties can be exposed directly in the Properties window (in the Data category, under DataBindings) and an Advanced option must be selected to bind any of the other properties. On a TextBox control, for instance, the Text and Tag properties are exposed as common data-bound properties. On a new user control that you have created, only Tag will be bindable directly in the property window unless you add attributes to specify which of your properties are likely to be data-bound. In the case of my SurveyRadioButtons control, I think that CurrentSelection will be the most likely data-binding property, so I will add an attribute to that property to make it appear in the Data section of the Properties window:
<System.ComponentModel.Category("Appearance"), _ System.ComponentModel.Bindable(True)> _ Public Property CurrentSelection() As Integer Get Return currentValue End Get Set(ByVal Value As Integer) If Value > 0 And Value < numButtons Then radioButtons(Value - 1).Checked = True End If End Set End Property
The underlying Microsoft Windows Forms engine can handle simple data binding more efficiently if there is a corresponding Changed event for the data-bound property, named <Property>Changed. By having this event, Windows Forms can use the event to determine when the property changes and update the underlying data source at that time; otherwise it updates the data source regardless of whether the property has changed. In the case of this control, there is a CurrentSelectionChanged event, so it will handle simple data binding of the CurrentSelection property in the most efficient manner.
Note Typically, it is not possible to data bind to the selected value of a group of radio buttons, but by building the group into a custom control I can expose and bind to the CurrentSelection property easily. There are many occasions where a data-bound group of radio buttons could be useful, so keep this sample in mind when building your user interfaces.
Complex data binding doesn't suit this particular control, but the next sample article, Extending the TreeView Control, covers a control that uses the full data-source method of binding.
By combining multiple controls into one custom control, you can create a complex mix of code and layout that can be easily packaged and distributed. This style of control is useful for something simple, such as combining a Label and a TextBox, or for something complex, such as a complete address entry control. Regardless of how you use this style of control, it is the closest in form to the ActiveX control development model of Visual Basic 5.0 and 6.0.
Note Like the SurveyRadioButtons control, but think it isn't flexible enough for your needs? You could implement a more flexible control if you built one where the number of choices was configurable, although the code would be more complex and you would have to choose between drawing your own radio buttons (using the ControlPaint class again) or dynamically creating a set of regular radio buttons and adding them to your control's surface in code. If you went down this route, supporting a variable number of choices, then complex data binding might be useful as a way to specify the set of possible choices. (You could bind to the set of possible choices to determine the number of radio buttons and at the same time bind the Value property to a different data source to save the answer chosen.)