I recently heard someone making skeptical statements about a Windows® Forms application and proclaiming surprise that "it even used data binding!" This made me reflect on the history of data binding over the past few generations of development tools. It brought to mind issues that existed across multiple versions of Visual Basic® and Microsoft® Access that left data binding with a bad reputation. Developers, especially the experienced ones, often see data binding as an inflexible and unreliable means of connecting a UI to data, as a technology best suited for demos but not for real-world use. In addition to these concerns, one of the biggest issues many people have with data binding is that it implies a tight coupling between the data and presentation layers of an application—a bad thing in many situations.
While some of these issues were valid in the past, data binding in Windows Forms has been designed to avoid or mitigate most of these concerns. The data-binding technology in the Microsoft .NET Framework is customizable, flexible, and (since you can bind to objects, not just to database systems) it does not require you to tie your interface directly to your back-end database. Thanks to these changes, I personally use data binding whenever possible, binding to a DataView or to a strongly typed collection in almost every single one of my projects.
With so many issues resolved I am able to concentrate on my wish list of features. The first I'd like to tackle is the absence of a data-bound control that would allow users to select from a list of radio buttons. In this column, I'm going to look at how you can create your own control to get around this omission from the Windows Forms classes.
Binding works by hooking to a single property value on a control, and there is no sort of "selected value" property on a group of radio buttons, so if you want to use this type of interface in your application, you have to handle it yourself. Having one set of controls that isn't data bound isn't a horrible situation, but it leads to special cases in your presentation layer code where you loop once for all of your bound controls, then add a little bit of special code for the one or more groups of radio buttons in your interface. In my experience, code in which you try to handle each case differently is difficult to maintain and the source of a majority of bugs.
A better solution would be to create a completely new type of control, a RadioButtonList, which would be the Windows Forms equivalent of the ASP.NET RadioButtonList control (see Figure 1), containing a SelectedValue property that you can bind to. This control would be bound in two ways, through a data source property for its list of options (the text and values for all of the radio buttons) and through the SelectedValue property for the value that has been selected. This type of data binding is used for all of the data-bound list controls in Windows Forms, including the ListBox and the ComboBox control, so this new control will be consistent with what is currently available.
Using Visual Basic 6.0, you had only one choice when developing your own control; you had to start with a blank User Control and add all of the functionality you needed. That is still an option in .NET, but it is generally not the best choice. You should try to base your new control on existing ones by inheriting from another control or class instead of from Control or UserControl. Of course, it isn't always possible, but in this case there is a class, ListControl, that provides the base implementation for both ComboBox and ListBox, and that will provide my new RadioButtonList control with all of the basic data-binding functionality I need:
Public Class RadioButtonList
Inherits ListControl
•••
End Class
Although ListControl provides great functionality (including managing the bound data source and providing a CurrencyManager instance to interact with), I still need to write the code to handle a few key issues. First I have to draw the control, which in this case means drawing a series of radio buttons. Also I must allow keyboard and mouse interaction with the control, including tracking which item in the list has focus (different from which item is selected). Finally, I have to handle resizing and need to provide a scrollbar if the control is too small for the number of items in the list.
The first task I'll tackle is the graphics work because until that is done I won't get anything drawn on the screen at all, and where's the fun in that? In my new control class, let's override the OnPaint routine and add a loop to draw the list of items as I did in Figure 2.
Walking through this code from top to bottom, you'll see that the first step is to clear the entire control to remove any pixels from having drawn a previous set of buttons or a different size control. After that, the border is drawn, using an internal variable m_borderStyle (I also added a property to the control to enable the user to set the border style, but I won't cover that in this column). Next, a transform is applied to the Graphics object (using the TranslateTransform method) to shift it according to the current position of the scrollbar (more on that in a bit). Finally, with everything all set up, the actual items (radio buttons) are drawn onto the control by looping through the data source (represented by the myList variable) and drawing each item out one at a time.
The DrawOneItem routine, as its name suggests, is called to draw each individual RadioButton onto the control:
Private Sub DrawOneItem(ByVal index As Integer, ByVal gr As Graphics)
To start, the code grabs the Font and ForeColor values from built-in properties of the control and uses them for all of its drawing, which ensures that the user has full control over the appearance of each item. Using the built-in properties has several advantages: you don't have to write any of your own code, the control doesn't get cluttered with additional properties, and you get any built-in behavior of the base properties. In the case of the Font and ForeColor properties, your control will automatically stay in sync with its container; changes to the corresponding properties on the form will be reflected by your control (assuming the user hasn't overridden that property on the control already):
Dim textFont As Font = Me.Font Dim textBrush As New SolidBrush(Me.ForeColor)
After grabbing the Font and ForeColor values, a StringFormat variable is created and configured to control how the Radio Button's caption will be drawn. In this case, the StringFormat is configured to align the text Near (which corresponds to left alignment if you are using a US English machine), but that alignment setting won't really have the desired effect because when DrawString is called later only a starting point is given, not a complete text area. Try changing it to Far if you want to see what happens. The text will be drawn so that it ends at that given starting point, which means that most of it won't even be visible on the control. FormatFlags controls how the text is clipped if it consists of more than one line; LineLimit indicates that only completely visible lines should be drawn:
Dim myStringFormat As New StringFormat myStringFormat.Alignment = StringAlignment.Near myStringFormat.FormatFlags = StringFormatFlags.LineLimit
This routine uses a variety of private variables to configure the layout of the button and the text (m_HorizontalSpacing and others), which is done to provide a bit more control to the user (a developer using this control), allowing him to use the corresponding control properties to tweak the control's appearance as he likes. Another internal variable, the rowStarts array, contains the vertical position of each item in the list and, since it is only calculated once when the control is resized, it is used to save time instead of having to recalculate a top position when each item is drawn. If you want to see all of the code that exposes these values as properties or that does the starting position calculation, then check out the complete source code for this class in the download available at the link at the top of this article.
Dim leftPos As Integer = m_HorizontalSpacing Dim topPos As Integer = Me.rowStarts(index)
The display text for the current item is the value of the DisplayMember property for the current object and accessing that value (which would involve property descriptors and a few lines of code) is simple thanks to the existence of a GetItemText function on the control's base class (ListControl).
'grab the display member text from the data source Dim itemText As String = Me.GetItemText(Me.DataManager.List.Item(index))
Now that I have the item text and can determine how much space it is going to take up on the control, it is possible to draw a focus rectangle to indicate if the current item is selected. Using a method of the ControlPaint class, which is a powerful set of utility functions designed for use when drawing your own controls, a dotted-line rectangle is drawn around the text if the item currently has the focus, as shown here:
'draw focus rectangle (dotted line)
If Me.focusedItem = index Then
Dim rowSize As SizeF
rowSize = gr.MeasureString(itemText, textFont, _
New PointF(leftPos, topPos), myStringFormat)
Dim focusRect As New Rectangle(leftPos, topPos, _
rowSize.Width + m_buttonSize, rowSize.Height)
focusRect.Inflate(m_focusRectInflation, m_focusRectInflation)
ControlPaint.DrawFocusRectangle(gr, focusRect)
End If
Then the RadioButton itself is drawn after setting up a ButtonState variable. ButtonState is an enumeration used with several methods of the ControlPaint class to indicate how a button (radio button, checkbox, and so on) should be drawn. In this case, the state of this RadioButton is affected by two things—whether or not the control is enabled and whether or not this particular radio button is selected:
'the ButtonState indicates how the Radio Button should be drawn
Dim bs As ButtonState
'should it be grayed out?
If Not Me.Enabled Then
bs = bs Or ButtonState.Inactive
End If
'should it be selected?
If Me.SelectedIndex = index Then
bs = bs Or ButtonState.Checked
End If
The combination of settings is then used to draw the actual control through the ControlPaint.DrawRadioButton method and then the Text is drawn to the left of the radio button. A nice addition to this control, which you might want to make if you were going to ship it out as part of a control library, would be to allow the user to configure the relative position of the text and of the radio button (enabling the button to be on the right-hand side of the text, for example):
ControlPaint.DrawRadioButton(gr, New Rectangle(leftPos, topPos, _
m_buttonSize, m_buttonSize), bs)
gr.DrawString(itemText, textFont, textBrush, leftPos + m_buttonSize, _
topPos, myStringFormat)
Your control isn't much use if the user can't click on it to select an item or navigate to it with the arrow and Tab keys on his keyboard. To handle both of these cases, you have to override the OnClick, OnKeyDown, and IsInputKey routines of your control. Focusing on the mouse interaction first, by capturing a mouse click via OnClick, you can determine which item in your list was clicked and make that item the currently selected one (see Figure 3).
Keyboard handling is a bit more complex. First, you have to override the IsInputKey routine to let the control know which key presses you are capable of handling, like so:
Protected Overrides Function IsInputKey( _
ByVal keyData As System.Windows.Forms.Keys) As Boolean
Select Case keyData
Case Keys.Down, Keys.Left, Keys.Up, _
Keys.Right, Keys.Enter, Keys.Return
Return True
Case Else
Return MyBase.IsInputKey(keyData)
End Select
End Function
Once you override IsInputKey, all of the indicated key presses will arrive through the OnKeyDown routine. Inside that routine, I don't do very much at all; I just pass the pressed key onto another routine, KeyPressed. That routine decides how the information about the pressed key should affect which item in the control gets the focus and what to do with the selected item if the user presses the return or space keys (see Figure 4).
I actually changed this keyboard-handling routine completely just before writing this column. Originally the selected item changed if the user moved up and down the list of radio buttons using the arrow keys, but I modified it so that the user had to explicitly select an item (using space or enter) and so that the arrow keys just moved the focus. This seems like a better user experience to me, but you can easily modify the code to match my original design if you prefer the other behavior.
Whenever you are creating a control that can be bound to a data list, you have to handle the situation in which you have more items than can fit in the control (see Figure 5).There, a vertical scrollbar is added if AutoSize = False and if the control is smaller than is needed to display the list of items.
For this control, I decided to handle the situation in two different ways: by automatically resizing the control (configurable with an AutoSize property) or by adding a vertical scrollbar as necessary (see Figure 5). Whenever the data source changes (because the number of items could have changed) or whenever the control is resized, the RecalcSizing routine is called to automatically resize the control or to add a scrollbar (see Figure 6).
Alternatively you could use AddHandler/RemoveHandler for tracking the ValueChanged event of the scrollbar, but by using WithEvents and Handles, Visual Basic .NET takes care of the event wiring for you.
The end result of all this code (and a few small snippets that are included in the download but don't merit listing within this column) is a data-bindable RadioButtonList control for Windows Forms. Almost any application can benefit from more features, and this control is no exception. You could expand it to handle checkboxes as well as radio buttons, you could add horizontal scrolling or word wrap on the captions, and lots more. As it's built now, it shows how you can achieve consistent use of data binding across your entire interface. For more information on Windows Forms development, check out Developing Custom Windows Controls Using Visual Basic .NET and http://www.windowsforms.net.
Send your questions and comments to basics@microsoft.com.