.NET 2 the Max

Databind Objects and Collections

Take advantage of the fact that the Windows Forms databinding mechanism can use any .NET object that implements the IList interface as the data source.

Technology Toolbox: VB.NET

Databinding is a key feature of the Microsoft .NET Framework, both in its Windows Forms and Web Forms flavors. Many .NET developers are quite familiar with Windows Forms databinding, especially those with previous VB6 experience; therefore, most articles and books focus on databinding in ASP.NET applications. Nevertheless, many facets of Windows Forms databinding aren't widely understood among developers. For example, many developers fail to appreciate how useful databinding can be even when they're not working with information stored in a database.

Developers typically use databinding in conjunction with ADO.NET objects, such as DataSet and DataView objects, but the Windows Forms databinding mechanism can use any .NET object that implements the IList interface as the data source, whether an array, an ArrayList, or a custom collection. Assume you have a Person class that exposes properties such as FirstName, LastName, BirthDate, and Married. You can create an array or a collection of Person objects and bind it to controls like this:

Dim WithEvents manager As _
   CurrencyManager

Sub InitializeBindings()
   ' persons is the array of Person 
   ' objects
   manager = _
      CType(Me.BindingContext( _
      persons), CurrencyManager)
   txtFirstName.DataBindings.Add( _
      "Text", persons, "FirstName")
   txtLastName.DataBindings.Add( _
      "Text", persons, "LastName")
   txtBirthDate.DataBindings.Add( _
      "Text", persons, "BirthDate")
   chkMarried.DataBindings.Add( _
      "Checked", persons, "Married")
   UpdateControlState()
End Sub

The InitializeBindings procedure is invoked before the form becomes visible, typically in the Form_Load event. Its first statement assigns a value to the form-level manager variable; you can use the CurrencyManager.Position property to navigate through all the rows in the data source. The UpdateControlState procedure tests this property and enables or disables the navigation buttons accordingly:

Sub UpdateControls()
   btnFirst.Enabled = _
      manager.Position > 0
   btnPrevious.Enabled = _
      manager.Position > 0
   btnNext.Enabled = _
      manager.Position < _ 
      manager.Count-1
   btnLast.Enabled = _
      manager.Position < _
      manager.Count-1
   lblRecord.Text = _
      String.Format("{0} of {1}", _
      manager.Position + 1, _
      manager.Count)
End Sub

You navigate through the set of values by assigning a new value to the Position property of the CurrencyManager object. Typically, you do this when the user clicks on a button or a toolbar button:

Private Sub btnNext_Click(ByVal sender _
   As Object, ByVal e As EventArgs) _
   Handles btnNext.Click
   manager.Position += 1
End Sub

You declare the CurrencyManager object with the WithEvents keyword, enabling you to trap its PositionChanged event, which in turn gives you a chance to update the state of your navigational controls:

Sub manager_PositionChanged(sender As _
   Object, e As EventArgs) Handles _
   manager.PositionChanged
   UpdateControls()
End Sub

Note that implementing basic databinding requires understanding only a few concepts.

You must add code to support other common operations, such as deleting an element or adding a new object to the data source. Note that you must use an ArrayList or another kind of collection instead of a standard array if you want to support additions and deletions, because arrays have fixed sizes.

Add Design-Time Support
One annoying problem of the approach just described is that you must write the code that creates the actual Binding objects. It's easy to make a mistake that would cause a runtime error or introduce a subtle bug, because you must type in all property names. It would be great if you could bind a control to a business object at design time, just as you do when you bind to a data set. Fortunately, adding support for design-time binding isn't hard at all.

The companion code for this column includes a PersonCollection object that derives from System.ComponentModel.Component. You can drop this object on a form's surface, and the fact that it implements the IList collection gives you support for databinding. The PersonCollection class wraps a private ArrayList object. The PersonCollection class's IList properties and methods do nothing but delegate to a member of the inner ArrayList. PersonCollection is a strongly typed collection and exposes an Item property that returns a Person object (see Listing 1).

You must compile the Person and the PersonCollection classes in a separate DLL; this enables you to later add a PersonCollection object to the Windows Forms toolbox and drop an instance of it on a form's surface. Visual Studio queries the return type of the Item property at design time and displays the list of fields available for binding (see Figure 1).

The PersonCollection class contains a nontrivial amount of code, but its structure is simple. Once you test this class, you can create other collections easily. For example, you can create an OrderCollection class that contains Order objects by replacing each occurrence of the Person string with the name of a different business object. Or, you can create a ListComponentBase class that contains most of the code, and then use inheritance to derive strongly typed collections.

Another important benefit of binding to a custom component: A component can host other components. For example, the PersonCollection might host an ADO.NET connection and one or more ADO.NET command objects to load and save its data as necessary. Or, it might load its elements from a file or through a Web service. In general, delegating the load and save operations to a component (rather than performing them from inside the form) improves the architecture of your application and lets you adopt a multitier approach with minimal impact on the client application.

Note that the PersonCollection component can also work as the data source of a DataGrid control. In this simple implementation, it allows the end users to edit only individual cells, at which point the component propagates changes to the underlying object, but it forbids add and delete operations. Keep in mind that a data source must implement the IBindingList interface to permit these operations automatically.

Format Bound Data
Each Binding object exposes two events: the Format event, which fires when a value is moved from the data source to the control, and the Parse event, which fires when the data in the control is moved back into the data source. Note that the Parse event fires only if the current row has been modified. These two events add a remarkable degree of flexibility to databinding.

For example, you often need to display null values in a special way, rather than as the empty string that the databinding infrastructure uses by default. Assume you want to store the special data value 9/9/9999 when the user leaves the txtBirthDate control empty, as often happens in database fields that don't allow nulls. First, define the handlers for the two events:

Private Sub DataFormat(ByVal sender As _
   Object, ByVal e As ConvertEventArgs)
   If e.Value.Equals(#9/9/9999#) Then
      e.Value = ""
   Else
      e.Value = String.Format( _
         "{0:dd-MM-yyyy}", e.Value)
   End If
End Sub

Private Sub DataParse(ByVal sender As _
   Object, ByVal e As ConvertEventArgs)
   If e.Value.Equals("") Then
      e.Value = #9/9/9999#
   Else
      e.Value = CDate(e.Value)
   End If
End Sub

Next, add these lines to the InitializeBindings procedure to trap the events:

Dim bnd As Binding = _
   txtBirthDate.DataBindings("Text")
AddHandler bnd.Format, AddressOf _
   DataFormat
AddHandler bnd.Parse, AddressOf _
   DataParse

The Format and Parse events are especially useful for displaying DateTime values in a format other than the default long date-time format, as well as for converting DbNull values read from databases. But you can also affect other properties of the bound control. For example, you can modify the DataFormat procedure to display null values with a cyan background color:

Dim bnd As Binding = CType(sender, _
   Binding)
If e.Value.Equals(#9/9/9999#) Then
   e.Value = ""
   e.Control.BackColor = Color.Cyan
Else
   e.Value = String.Format( _
      "{0:dd-MM-yyyy}", e.Value)
   e.Control.BackColor = Color.White
End If

Note that you could access the txtBirthDate control directly in this specific case. However, querying the Control property of the Binding object gives you more flexibility and allows you to reuse the same Format handler for different bound controls.

The databinding mechanism is highly generic and you aren't limited to using it only with data, whether ADO.NET objects or arrays of business objects. In fact, you can use databinding to reduce the amount of code in your forms. For example, assume you want to enable all the controls in a Panel when the end user clicks on a CheckBox control. The standard approach requires that you write a line of code in the CheckBox's CheckedChanged event handler:

Sub CheckBox1_CheckedChanged(sender As _
   Object, e As EventArgs)
   Panel1.Enabled = CheckBox1.Checked
End Sub

You can achieve the same result by binding the Panel's Enabled property to the CheckBox's Checked property by adding this statement to the Form_Load event:

Panel1.DataBindings.Add("Enabled", _
   CheckBox1, "Checked")

You can use this technique in a few other cases, but you should be aware that creating relationships between controls in this nonstandard way tends to make debugging and testing a bit harder.

Define Your Data Source
Many VB6 developers complain about the lack of the FileListBox, DirListBox, and DriveListBox controls in the .NET Framework. Fortunately, you can use databinding to work around these shortcomings easily and achieve even more functionality than the original VB6 controls offered.

The companion code for this column contains a FileInfoCollection component that can fill a private ArrayList object with one FileInfo object for each file found in a given directory. The Path, IncludeSubdirectories, and Pattern properties let you specify which files should be added to the list. The FileInfoCollection component implements the IList interface, so you can bind other controls to the properties of the FileInfo object, including FullName, Name, Length, and so on. You can even use this approach to fill ListBox or ComboBox controls automatically with the names of all the files in a folder or in a directory tree.

Let's create a simple image viewer to demonstrate how easy this mechanism is in practice. First, drop an instance of the FileInfoCollection object on the form's surface and set its Path property to C:\Windows and its Pattern property to the "*.bmp|*.jpg|*.gif" string. Next, drop a ListBox control on the form, set its DataSource property equal to FileInfoCollection1, its DisplayMember property equal to Name, and its ValueMember property to FullName. Finally, create a Label control, bind its Text property to the FileInfoCollection1.FullName property, and add a PictureBox control to display the selected image (see Figure 2).

The binding infrastructure fills the FileInfoCollection object at run time with a collection of FileInfo objects that contains information on all the image files in the main Windows directory; consequently, the list of their names appears in the ListBox control. When the end user selects a ListBox element, the full name of the file appears in the Label control. Type this statement in the ListBox's SelectedIndexChanged event handler to display the corresponding image in the PictureBox:

Me.PictureBox1.Image = Image.FromFile( _
   CStr(ListBox1.SelectedValue))

You could even implement the image viewer program without trapping the SelectedIndexChanged event if the standard PictureBox control were able to load an image file by assigning a string property. Fortunately, creating an improved PictureBox with this added feature is a breeze, thanks to inheritance (see Listing 2). The BoundPictureBox control inherits from the standard PictureBox control and adds a single new property, ImageFile. If this property is assigned a filename, the control loads and displays the corresponding image.

The new ImageFile property is marked with the Bindable attribute, so the property is included in the list of properties that appears when you expand the DataBindings element in the Properties window. This means you can replace the standard PictureBox control with a BoundPictureBox and bind the latter's ImageFile to the FileInfo's FullName. You can now implement the image viewer without writing a single line of code.

About the Author

Francesco Balena has authored several programming books, including Programming Microsoft Visual Basic .NET Version 2003 [Microsoft Press]. He speaks regularly at VSLive! and other conferences, founded the .Net2TheMax family of sites, and is the principal of Code Architects Srl, an Italian software company that offers training and programming tools for .NET developers. Reach him at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube