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 string Name {     get { return name; }     set { name = value; }   }   public int Number {     get { return number; }     set { number = value; }   }   string name = "Chris";   int number = 452; } NameAndNumber source = new NameAndNumber(); void CustomItemDataSourceForm_Load(object sender, EventArgs e) {  // Bind to public properties   textBox1.DataBindings.Add("Text", source, "Name");   textBox2.DataBindings.Add("Text", source, "Number");  } 

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 EventHandler  <propertyName>Changed  ; 

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

 class NameAndNumber {  // For bound controls   public event EventHandler NameChanged;  public string Name {     get { return name; }     set {       name = value;  // Notify bound control of changes   if( NameChanged != null ) NameChanged(this, EventArgs.Empty);  }   }   ... } 

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

 void setNameButton_Click(object sender, EventArgs e) {  // Changes replicated to textBox1.Text   source.Name = "Joe";  } 

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 :  ICustomTypeDescriptor  {  public PropertyDescriptorCollection GetProperties(...) {   // Expose no properties for run-time binding   return new PropertyDescriptorCollection(null);   }  ... } 

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 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 Fraction(int numerator, int denominator) {     this.numerator = numerator;     this.denominator = denominator;   }   public int Numerator {     get { return this.numerator; }     set { this.numerator = value; }   }   public int Denominator {     get { return this.denominator; }     set { this.denominator = value; }   }   int numerator;   int denominator; } class NameAndNumber {   ...   // Expose a custom type from a bound property   public event EventHandler NumberChanged;   public Fraction Number {     get { return number; }     set {       number = value;       // Notify bound control of changes       if( NumberChanged != null ) NumberChanged(this, EventArgs.Empty);     }   }   string name = "Chris";   Fraction number = new Fraction(452, 1); } NameAndNumber source = new NameAndNumber(); void CustomItemDataSourceForm_Load(object sender, EventArgs e) {   // Bind to a custom type   textBox2.DataBindings.Add("Text", source, "Number"); } 

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 override string ToString() {  return string.Format("{0}/{1}", numerator, denominator);  }  } 
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 :  TypeConverter  {  public override bool   CanConvertFrom(   ITypeDescriptorContext context,   Type sourceType) {  return sourceType == typeof(string);  }   public override bool   CanConvertTo(   ITypeDescriptorContext context,   Type destinationType) {  return destinationType == typeof(string);  }   public override object   ConvertFrom(   ITypeDescriptorContext context,   CultureInfo culture,   object value) {  // Very simple conversion ignores context, culture, and errors     string from = (string)value;     int slash = from.IndexOf("/");     int numerator = int.Parse(from.Substring(0, slash));     int denominator = int.Parse(from.Substring(slash + 1));     return new Fraction(numerator, denominator);  }   public override object   ConvertTo(   ITypeDescriptorContext context,   CultureInfo culture,   object value,   Type destinationType) {  if( destinationType != typeof(string) ) return null;     Fraction number = (Fraction)value;     return string.Format(       "{0}/{1}", number.Numerator, number.Denominator);  }  } 

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

  [TypeConverterAttribute(typeof(FractionTypeConverter))]  class Fraction {...} 

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:

 NameAndNumber source = new NameAndNumber(); void CustomItemDataSourceForm2_Load(object sender, EventArgs e) {   textBox1.DataBindings.Add("Text", source, "Name");  // Bind to a property of a custom type   // and handle the parsing   Binding binding =   textBox2.DataBindings.Add("Text", source, "Number");   binding.Parse += new ConvertEventHandler(textBox2_Parse);  }  void textBox2_Parse(object sender, ConvertEventArgs e) {  string from = (string)e.Value;   int slash = from.IndexOf("/");   int numerator = int.Parse(from.Substring(0, slash));   int denominator = int.Parse(from.Substring(slash + 1));   e.Value = new Fraction(numerator, denominator);  }  

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:

 void textBox2_Parse(object sender, ConvertEventArgs e) {  // Let the type converter do the work   TypeConverter converter = TypeDescriptor.GetConverter(e.DesiredType);   if( converter == null ) return;   if( !converter.CanConvertFrom(e.Value.GetType()) ) return;   e.Value = converter.ConvertFrom(e.Value);  } 

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 : Binding {   public WorkAroundBinding(string name, object src, string member)     : base(name, src, member) {   }  protected override void OnParse(ConvertEventArgs e) {  try {       // Let the base class have a crack       base.OnParse(e);     }     catch( 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)          (e.Value.GetType() == e.DesiredType)          (e.Value is DBNull) ) {         return;       }       // Ask the desired type for a type converter       TypeConverter converter =         TypeDescriptor.GetConverter(e.DesiredType);       if( (converter !=  null) &&           converter.CanConvertFrom(e.Value.GetType()) ) {         e.Value = converter.ConvertFrom(e.Value);       }     }  }  } 

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

 void Form1_Load(object sender, EventArgs e) {   textBox1.DataBindings.Add("Text", source, "Name");   // Let the custom binding handle the parsing workaround  Binding binding =   new WorkAroundBinding("Text", source, "Number");   textBox2.DataBindings.Add(binding);  } 

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 complex bound controls, such as the ListBox or the ComboBox, requires only a collection of your custom data source objects:

  NameAndNumber[] source =  { new NameAndNumber("John", 8), new NameAndNumber("Tom", 7) }; void ComplexCustomDataSourceForm_Load(object sender, EventArgs e) {  // Bind a collection of custom data sources complexly   listBox1.DataSource = source;  } 

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 override string ToString() {  return this.name + ", " + this.number;  }  } void CustomListDataSourceForm_Load(object sender, EventArgs e) {  // Let the object determine how it's displayed   listBox1.DataSource = source;  } 

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:

 NameAndNumber[] source = { new NameAndNumber("John", 8), new NameAndNumber("Tom", 7) }; void ComplexCustomDataSourceForm_Load(object sender, EventArgs e) {   listBox1.DataSource = source;  listBox1.DisplayMember = "Name";   listBox1.ValueMember = "Number";  } void listBox1_SelectedIndexChanged(object sender, EventArgs e) {  // Show selected value as selected index changes   statusBar1.Text = "selected Number= " + listBox1.SelectedValue;  } 

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 Fraction(int numerator, int denominator) {     this.numerator = numerator;     this.denominator = denominator;   }   public int Numerator {     get { return this.numerator; }     set { this.numerator = value; }   }   public int Denominator {     get { return this.denominator; }     set { this.denominator = value; }   }   int numerator;   int denominator; } // Top-level objects: class NameAndNumbers {   public NameAndNumbers(string name) {     this.name = name;   }   public event EventHandler NameChanged;   public string Name {     get { return name; }     set {       name = value;       if( NameChanged != null ) NameChanged(this, EventArgs.Empty);     }   }  // Expose second-level hierarchy:   // NOTE: DataGrid doesn't bind to arrays   public ArrayList Numbers {   get { return this.numbers; }   }   // Add to second-level hierarchy   public void AddNumber(Fraction number) {   this.numbers.Add(number);   }  public override string ToString() {     return this.name;   }   string name = "Chris";  ArrayList numbers = new ArrayList(); // subobjects  } 

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:  ArrayList source = new ArrayList(); void CustomListDataSourceForm2_Load(object sender, EventArgs e) {   // Populate the hierarchy   NameAndNumbers nan1 = new NameAndNumbers("John");   nan1.AddNumber(new Fraction(1, 2));   nan1.AddNumber(new Fraction(2, 3));   nan1.AddNumber(new Fraction(3, 4));   source.Add(nan1);   NameAndNumbers nan2 = 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; } 

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 C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

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