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 SourcesThe 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 BindingThe 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:
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.
Type ConversionNotice 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 SourcesThe 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 HierarchiesSometimes 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. |