Custom Data Sources


Although the data set is a popular data source, it's by no means the only one. Any custom type can be a data source.

Custom Item Data Sources

The requirements of an item data source are only that it expose one or more public properties:

 
 ' Expose two properties for binding Class NameAndNumber   Public Property Name() As String       Get           Return myName       End Get       Set           myName = Value       End Set   End Property   Public Property Number() As Integer       Get           Return myNumber       End Get       Set           myNumber = Value       End Set   End Property   Dim myName As String = "Chris"   Dim myNumber As Integer = 452 End Class Dim source As NameAndNumber = New NameAndNumber() Sub CustomItemDataSourceForm_Load(sender As Object, e As EventArgs)   ' Bind to public properties   textBox1.DataBindings.Add("Text", source, "Name")   textBox1.DataBindings.Add("Text", source, "Number") End Sub 

In this case, we've created a simple class that exposes two public properties and binds each of them to the Text property of two text boxes, as shown in Figure 13.35.

Figure 13.35. Binding to a Custom Item Data Source

The binding works just as it did when we implemented binding against columns in a table. Changes in the bound control will be used to set the value of read/write properties on the object. However, by default, the binding doesn't work the other way; changes to the data source object won't automatically be reflected to the bound controls. Enabling that requires the data source to expose and fire an event of type EventHandler to which the binding can subscribe. Each property that must notify a bound control of a change must expose an event using the following naming convention:

 
 Public Event <propertyName>Changed As EventHandler 

When firing, the data source object passes itself as the sender of the event:

 
 Class NameAndNumber   ' For bound controls   Public Event NameChanged As EventHandler   Public Property Name() As String       Get           Return myName       End Get       Set           myName = Value           ' Notify bound control of changes           RaiseEvent NameChanged(Me, EventArgs.Empty)       End Set   End Property   ... End Class 

Now, when the data source object's properties are changed, bound controls are automatically updated:

 
 Sub setNameButton_Click(sender As Object, e As EventArgs)   ' Changes replicated to textBox1.Text   source.Name = "Joe" End Sub 

Type Descriptors and Data Binding

The information that the property manager uses to determine the list of properties that can be used for binding is provided by the TypeDescriptor class from the System.ComponentModel namespace. The TypeDescriptor class uses .NET Reflection [7] to gather information about an object's properties. However, before falling back on Reflection, the TypeDescriptor class first calls the GetProperties methods of the ICustomTypeDescriptor interface to ask the object for a list of properties. The list of PropertyDescriptor objects returned from this method can expose a custom set of properties or no properties at all:

[7] For a discussion of .NET Reflection, see Essential .NET (Addison-Wesley, 2003), by Don Box, with Chris Sells.

 
 Class NameAndNumber   Implements ICustomTypeDescriptor   Public Function GetProperties(...) As PropertyDescriptorCollection       ' Expose no properties for run-time binding       Return New PropertyDescriptorCollection(Nothing)   End Function   ... End Class 

For example, this implementation of GetProperties causes any call to the AddBinding method on our custom data source to fail at run time, because the list of properties exposed from the class is empty. Exposing a subset of properties for binding requires passing an array of objects of a custom type that derives from the PropertyDescriptor base class. [8] If you're willing to go that far, you can use the same technique to build properties at run time out of whole cloth from external data. This is how DataRowView exposes columns on tables for binding that it doesn't know about until run time.

[8] Unfortunately, .NET provides no public, creatable classes that derive from PropertyDescriptor, but TypeConverter.SimplePropertyDescriptor is a nice start.

Type Conversion

Notice that our custom data source class uses simple types that are easy to convert to and from strings. That's important because the Text property of a control is a string.

However, you don't always want to bind simple types to string properties. For example, you may need to bind to a custom data type from a property instead of binding to a simple type. For that to work, the data must be convertible not only to a string but also back from a string to the custom data type. Otherwise, the changes that the user makes to the bound control will be lost. For example, consider beefing up the sample code to expose a custom type as the value to one of the properties being bound to:

 
 Class Fraction   Public Sub New(numerator As Integer, denominator As Integer)       Me.Numerator = numerator       Me.Denominator = denominator   End Sub   Public Property Numerator() As Integer       Get           Return Me.myNumerator       End Get       Set           Me.myNumerator = Value       End Set   End Property   Public Property Denominator() As Integer       Get           Return Me.myDenominator       End Get       Set           Me.myDenominator = Value       End Set   End Property   Dim myNumerator As Integer   Dim myDenominator As Integer End Class Class NameAndNumber   ...   ' Expose a custom type from a bound property   Public Event NumberChanged As EventHandler   Public Property Number() As Fraction       Get           Return myNumber       End Get       Set           myNumber = Value           ' Notify bound control of changes           RaiseEvent NumberChanged(Me, EventArgs.Empty)       End Set   End Property   Dim name As String = "Chris"   Dim myNumber As Fraction = New Fraction(452, 1) End Class Dim source As NameAndNumber = New NameAndNumber() Sub CustomItemDataSourceForm_Load(sender As Object, e As EventArgs)   ' Bind to a custom type   textBox2.DataBindings.Add("Text", source, "Number") End Sub 

By default, this binding will show the name of the type instead of any meaningful value, as shown in Figure 13.36.

Figure 13.36. Binding to a Custom Item Data Source without a Conversion to String

To get the string value to be set as the Text property, the binding falls back on the ToString method of the custom Fraction class, which defaults to the Object base class's implementation of returning the name of the type. Overriding the ToString method of the Fraction class solves the display problem (as shown in Figure 13.37):

 
 Class Fraction   ...   Public Overrides Function ToString() As String       Return String.Format("{0}/{1}", Me.Numerator, _           Me.Denominator)   End Function End Class 
Figure 13.37. Binding to a Custom Item Data Source with a Conversion to String

However, implementing ToString fixes only half of the conversion problem. The other half is taking the value that the user enters and putting it back into the property. There is no method to override in the Fraction class that will allow that. Instead, you'll need a type converter.

As discussed in Chapter 9: Design-Time Integration, a type converter is a class that derives from the TypeConverter class in the System.ComponentModel namespace. Specifically, the virtual methods that you must override when converting between types are the CanConvertFrom and CanConvertTo methods and the ConvertFrom and ConvertTo methods. This is an example of a type converter that knows how to convert between our custom Fraction type and the string type:

 
 Class FractionTypeConverter   Inherits TypeConverter   Public Overloads Overrides Function CanConvertFrom( _       context As ITypeDescriptorContext, sourceType As Type)_   As Boolean       Return sourceType = GetType(String)   End Function   Public Overloads Overrides Function CanConvertTo( _       context As ITypeDescriptorContext, _ destinationType As Type) As Boolean Return destinationType = GetType(String)   End Function   Public Overloads Overrides Function ConvertFrom( _       context As ITypeDescriptorContext, culture As CultureInfo, _       value As Object) As Object       ' Very simple conversion ignores context, culture, and errors       Dim from As String = CStr(value)       Dim slash As Integer = from.IndexOf("/")       Dim numerator As Integer = _           Integer.Parse(from.Substring(0, slash))       Dim denominator As Integer = _           Integer.Parse(from.Substring(slash + 1))       Return New Fraction(numerator, denominator)   End Function   Public Overloads Overrides Function ConvertTo( _       context As ITypeDescriptorContext, culture As CultureInfo, _       value As Object, destinationType As Type) As Object       If destinationType <> GetType(String) Then Return Nothing       Dim number As Fraction = CType(value, Fraction)       Return String.Format("{0}/{1}", number.Numerator, _           number.Denominator)   End Function End Class 

Associating the type converter with the type is a matter of applying TypeConverterAttribute:

 
 <TypeConverter(GetType(FractionTypeConverter))> _ Class Fraction   ... End Class 

Now, instead of using the ToString method to get the Fraction string to display in the bound control, the binding will use the FractionTypeConverter class's CanConvertTo and ConvertTo methods.

Similarly, when new data is available, the binding will use the CanConvertFrom and ConvertFrom methods. Or rather, it would, if that worked. Unfortunately, as of .NET 1.1, converting control data back into data of the custom type never makes it back to the custom type converter. Instead, an untested hunk of code throws an exception that's caught and ignored, and the conversion back from the control to the custom data type silently fails. One workaround is to handle the Binding class's Parse event, as discussed earlier in this chapter, and let the client do the conversion:

 
 Dim source As NameAndNumber = New NameAndNumber() Sub CustomItemDataSourceForm2_Load(sender As Object, e As EventArgs)   textBox1.DataBindings.Add("Text", source, "Name")   ' Bind to a property of a custom type   ' and handle the parsing   Dim myBinding As Binding = _       textBox2.DataBindings.Add("Text", source, "Number")   AddHandler myBinding.Parse, AddressOf textBox2_Parse End Sub Sub textBox2_Parse(sender As Object, e As ConvertEventArgs)   Dim from As String = CStr(e.Value)   Dim slash As Integer = from.IndexOf("/")   Dim numerator As Integer = Integer.Parse(from.Substring(0, slash))   Dim denominator As Integer = _       Integer.Parse(from.Substring(slash + 1))   e.Value = New Fraction(numerator, denominator) End Sub 

However, instead of building the parsing of the type into the form itself, it's much more robust to let the parsing be handled by the type's own type converter:

 
 Sub textBox2_Parse(sender As Object, e As ConvertEventArgs)   ' Let the type converter do the work   Dim converter As TypeConverter = _       TypeDescriptor.GetConverter(e.DesiredType)   If converter Is Nothing Then Exit Sub   If Not(converter.CanConvertFrom(e.Value.GetType())) Then Exit Sub   e.Value = converter.ConvertFrom(e.Value) End Sub 

Another solution that doesn't require manually handling the parsing of every property of a custom type is to derive from the Binding base class and override the OnParse method:

 
 Class WorkAroundBinding   Inherits Binding   Public Overloads Sub New(name As String, src As Object, member As _       String)       MyBase.New(name, src, member)   End Sub   Protected Overrides Sub OnParse(e As ConvertEventArgs)       Try           ' Let the base class have a crack           MyBase.OnParse(e)       Catch invCastEx As InvalidCastException           ' Take over for base class if it fails           ' If one of the base class event handlers               ' converted it, we're finished           If e.Value.GetType().IsSubclassOf(e.DesiredType) _               OrElse e.Value.GetType() = e.DesiredType OrElse _               TypeOf e.Value Is DBNull Then               Exit Sub           End If           ' Ask the desired type for a type converter           Dim converter As TypeConverter = _               TypeDescriptor.GetConverter(e.DesiredType)           If Not(converter Is Nothing) AndAlso _               Converter.CanConvertFrom(e.Value.GetType())) _               Then               e.Value = converter.ConvertFrom(e.Value)           End If       End Try   End Sub End Class 

You use the WorkAroundBinding class instead of the standard Binding class when adding a binding to a property of a custom type:

 
 Sub Form1_Load(sender As Object, e As EventArgs)   textBox1.DataBindings.Add("Text", source, "Name")   ' Let the custom binding handle the parsing workaround   Dim myBinding As Binding = _       New WorkAroundBinding("Text", source, "Number")   textBox2.DataBindings.Add(binding) End Sub 

Neither of these techniques is as nice as if custom types were fully supported, but both show the power that comes from understanding the basics of the data binding architecture.

List Data Sources

The basics of simple data binding support require only that you expose public properties. In the same way, setting the content for a complex bound controls, such as the ListBox or the ComboBox, requires only a collection of your custom data source objects:

 
 Dim source() As NameAndNumber = _   { New NameAndNumber("John", 8), New NameAndNumber("Tom", 7) } Sub ComplexCustomDataSourceForm_Load(sender As Object, e As EventArgs)   ' Bind a collection of custom data source complexly   listBox1.DataSource = source End Sub 

As you recall from the earlier discussion, setting only the DataSource and not the DisplayMember property shows the type of each object instead of anything useful from them, as shown in Figure 13.38.

Figure 13.38. Binding to a Custom List Data Source without a String Conversion

If you'd like the object itself to decide how it's displayed, you can leave the DisplayMember property as null and let the type's implementation of ToString or its type converter determine what's shown:

 
 Class NameAndNumber   ...   ' For use by single-valued complex controls,   ' e.g., ListBox and ComboBox   Public Overrides Function ToString() As String       Return Me.name & ", " & Me.number.ToString()   End Function End Class Sub CustomListDataSourceForm_Load(sender As Object, e As EventArgs)   ' Let the object determine how it's displayed   listBox1.DataSource = source End Sub 

Binding to a collection of NameAndNumber objects with its own ToString implementation yields the kind of display shown in Figure 13.39

Figure 13.39. Binding to a Custom List Data Source with a String Conversion

The ToString method is used only in the absence of values in both the DisplayMember and the ValueMember properties. When used against custom data sources, the list controls allow the DisplayMember and ValueMember properties to be set with the names of properties to pull from the custom types in the collection:

 
 Dim source() As NameAndNumber = _   { New NameAndNumber("John", 8), New NameAndNumber("Tom", 7) } Sub ComplexCustomDataSourceForm_Load(sender As Object, e As EventArgs)   listBox1.DataSource = source   listBox1.DisplayMember = "Name"   listBox1.ValueMember = "Number" End Sub Sub listBox1_SelectedIndexChanged(sender As Object, e As EventArgs)   ' Show selected value as selected index changes   statusBar1.Text = "selected Number= " & listBox1.SelectedValue End Sub 

The results of this binding are shown in Figure 13.40.

Figure 13.40. Binding Properties as Data Members

Binding to Custom Hierarchies

Sometimes objects are arranged in hierarchies. You can accommodate this using a property that exposes a collection class. For example, you might use an ArrayList, which implements the IList interface from the System.Collections namespace:

 
 ' Lowest-level objects Class Fraction   Public Sub New(numerator As Integer, denominator As Integer)       Me.Numerator = numerator       Me.Denominator = denominator   End Sub   Public Property Numerator() As Integer       Get           Return Me.myNumerator       End Get       Set           Me.myNumerator = Value       End Set   End Property   Public Property Denominator() As Integer       Get           Return Me.myDenominator       End Get       Set           Me.myDenominator = Value       End Set   End Property   Dim myNumerator As Integer   Dim myDenominator As Integer End Class ' Top-level objects Class NameAndNumbers   Public Sub New(name As String)       Me.Name = name   End Sub   Public Event NameChanged As EventHandler   Public Property Name() As String       Get           Return myName       End Get       Set           myName = Value           RaiseEvent NameChanged(Me, EventArgs.Empty)       End Set   End Property   ' Expose second-level hierarchy   ' NOTE: DataGrid doesn't bind to arrays   Public ReadOnly Property Numbers() As ArrayList       Get           Return Me.myNumbers       End Get   End Property   ' Add to second-level hierarchy   Public Sub AddNumber(number As Fraction)       Me.Numbers.Add(number)   End Sub   Public Overrides Function ToString() As String       Return Me.myName   End Function   Dim myName As String = "Chris"   Dim myNumbers As ArrayList = New ArrayList() ' sub-objects End Class 

When you've got a multilevel hierarchy of custom types, you can bind it against any control that supports hierarchies, such as the DataGrid control:

 
 ' Top-level collection: Dim source As ArrayList = New ArrayList() Sub CustomListDataSoruceForm2_Load(sender As Object, e As EventArgs)   ' Populate the hierarchy   Dim nan1 As NameAndNumbers = New NameAndNumbers("John")   nan1.AddNumber(New Fraction(1, 2))   nan1.AddNumber(New Fraction(2, 3))   nan1.AddNumber(New Fraction(3, 4))   source.Add(nan1)   Dim nan2 As NameAndNumbers = New NameAndNumbers("Tom")   nan2.AddNumber(New Fraction(4, 5))   nan2.AddNumber(New Fraction(5, 6))   source.Add(nan2)   ' Bind a collection of custom data sources complexly   dataGrid1.DataSource = source End Sub 

Figure 13.41 shows the top-level collection bound to a data grid, showing a link to the second-level collection.

Figure 13.41. Showing the Top Level of an Object Hierarchy in a Data Grid

The data grid shows each public property from the object at each level, along with links to subobjects exposed as collection properties. Figure 13.42 shows the second-level collection after the link has been followed.

Figure 13.42. Showing the Second Level of an Object Hierarchy in a Data Grid

The data grid is pretty flexible, but each level of hierarchy needs to be a type that exposes one or more public properties. It needs property names for column names. This means that, unlike the list controls, a data grid won't use a type converter or the ToString method to show the value of the object as a whole.

A more full-featured integration with the data grid ”including enabling the data grid UI to edit the data and the ability to keep the data grid up-to-date as the list data source is modified ”requires an implementation of the IBindingList interface, something that is beyond the scope of this book.



Windows Forms Programming in Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net