When you select a component on a design surface, the entries in the Properties window are rendered from an internal instance of that component. When you edit properties in the Properties window, the component instance is updated with the new property values. This synchronicity isn't as straightforward as it seems, however, because the Properties window displays properties only as text, even though properties can be of any type. As values shuttle between the Properties window and the component instance, they must be converted back and forth between the string type and the type of the property. Enter the type converter, the translator droid of .NET, whose main goal in life is to convert between types. For string-to-type conversion, a type converter is used for each property displayed in the Properties window, as shown in Figure 11.17. Figure 11.17. The Properties Window and Design-Time Conversion.NET offers the TypeConverter class (from the System.ComponentModel namespace) as the base implementation type converter. And .NET also gives you several derivationsincluding StringConverter, Int32Converter, and DateTimeConverterthat support conversion between one common .NET type and one or more other common .NET types. If you know the type that needs conversion at compile time, you can create an appropriate converter directly: // Type is known at compile time TypeConverter converter = new Int32Converter(); Or, if you don't know the type that needs conversion until run time, let the TypeDescriptor class (from the System.ComponentModel namespace) make the choice for you: // Don't know the type before run time object myData = 0; TypeConverter converter = TypeDescriptor.GetConverter(myData.GetType()); The TypeDescriptor class provides information about a particular type or object, including methods, properties, events, and attributes. TypeDescriptor.GetConverter evaluates a type to determine a suitable TypeConverter based on the following:
Because the Properties window is designed to display the properties of any component, it can't know specific property types in advance. Consequently, it relies on TypeDescriptor. GetConverter to dynamically select the most appropriate type converter for each property. After a type converter is chosen, the Properties window and the component instance perform the required conversions, using the same principle expressed in the following code: public void PretendDesignerConversion() { // Create the appropriate type converter object myData = 0; TypeConverter converter = TypeDescriptor.GetConverter(myData.GetType()); // Can converter convert int to string? if( converter.CanConvertTo(typeof(string)) ) { // Convert it object intToString = converter.ConvertTo(42, typeof(string)); } // Can converter convert string to int? if( converter.CanConvertFrom(typeof(string)) ) { // Convert it object stringToInt = converter.ConvertFrom("42"); } } When the Properties window renders itself, it uses the type converter to convert each component instance property to a string representation using the following steps:
The string representation of the source value is then displayed at the property's entry in the Properties window. If the property is edited and the value is changed, the Properties window uses the next steps to convert the string back to the source property value:
Some intrinsic type converters can do more than just convert between simple types. To demonstrate, let's upgrade AlarmClockControl's ShowDigitalTime property to a new Face property of type ClockFace that allows developers to decide how the clock is displayed, including options for Analog, Digital, or Both: // ClockFaceEnumeration.cs enum ClockFace { Analog = 0, Digital = 1, Both = 2 } // AlarmClockControl.cs partial class AlarmClockControl : ... { ... ClockFace face = ClockFace.Both; ... [Category("Appearance")] [Description("Determines the clock face type to display.")] [DefaultValue(ClockFace.Both)] public ClockFace Face { get {...} set {...} } ... } TypeDescriptor.GetConverter returns an EnumConverter, which has the smarts to examine the source enumeration and convert it to a drop-down list of descriptive string values, as shown in Figure 11.18. Figure 11.18. Enumeration Type Displayed in the Properties Window via EnumConverter
Custom Type ConvertersAlthough the built-in type converters are useful, they aren't enough when components expose properties based on custom types, such as AlarmClockControl's HourHand, MinuteHand, and SecondHand properties: // Hand.cs class Hand { Color color = Color.Black; int width = 1; public Hand(Color color, int width) { this.color = color; this.width = width; } public Color Color { get {...} set {...} } public int Width { get {...} set {...} } } // AlarmClockControl.cs partial class AlarmClockControl : ... { ... Hand hourHand = new Hand(Color.Black, 1); Hand minuteHand = new Hand(Color.Black, 1); Hand secondHand = new Hand(Color.Red, 1); ... [Category("Appearance")] [Description("Sets the color and size of the Hour Hand.")] public Hand HourHand { get {...} set {...} } [Category("Appearance")] [Description("Sets the color and size of the Minute Hand.")] public Hand MinuteHand { get {...} set {...} } [Category("Appearance")] [Description("Sets the color and size of the Second Hand.")] public Hand SecondHand { get {...} set {...} } ... } The idea is to give developers the option to pretty up the clock's hands with color and width values. If we had no custom type converter, the result would be rather unfortunate, as shown in Figure 11.19.[6]
Figure 11.19. Complex Properties in the Properties Window
Just as the Properties window can't know which types it will display, .NET can't know which custom types you'll develop. Consequently, there aren't any type converters capable of handling your custom types. However, the type converter infrastructure is extensible enough that you can leverage it to provide your own. To build a custom type converter, you start by deriving from the TypeConverter base class: // HandConverter.cs class HandConverter : TypeConverter {...} To support conversion, HandConverter must override CanConvertTo, CanConvertFrom, ConvertTo, and ConvertFrom: // HandConverter.cs class HandConverter : TypeConverter { public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType) {...} public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType) {...} public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo info, object value) {...} public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {...} } CanConvertFrom lets clients know which types it can convert from. In this case, HandConverter reports that it can convert from a string type to a Hand type: // HandConverter.cs class HandConverter : TypeConverter { public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType) { // We can convert from a string to a Hand type if( sourceType == typeof(string) ) { return true; } return base.CanConvertFrom(context, sourceType); } } Whether the string type is in the correct format is left up to ConvertFrom, which actually performs the conversion. HandConverter expects a multivalued string, which it splits into its atomic values before it uses them to instantiate a Hand object: // HandConverter.cs class HandConverter : TypeConverter { ... public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo info, object value) { // If converting from a string if( value is string ) { // Build a Hand type try { // Get Hand properties string propertyList = (string)value; string[] properties = propertyList.Split(';'); return new Hand(Color.FromName(properties[0].Trim()), Convert.ToInt32(properties[1])); } catch { } throw new ArgumentException("The arguments were not valid."); } return base.ConvertFrom(context, info, value); } } To convert a Hand type back to a string, we first need to let clients know we can, and we do that by overriding CanConvertTo: // HandConverter.cs class HandConverter : TypeConverter { ... public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType) { // We can convert from a Hand type to a string return (destinationType == typeof(string)); } } Then, we override ConvertTo to perform the actual conversion: // HandConverter.cs class HandConverter : TypeConverter { ... public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { // If source value is a Hand type if( value is Hand ) { // Convert to string if( (destinationType == typeof(string)) ) { Hand hand = (Hand)value; string color; if( hand.Color.IsNamedColor ) { color = hand.Color.Name; } else { color = string.Format("{0}, {1}, {2}", hand.Color.R, hand.Color.G, hand.Color.B); } return string.Format("{0}; {1}", color, hand.Width.ToString()); } } // Base ConvertTo if string isn't required return base.ConvertTo(context, culture, value, destinationType); } } When the Properties window looks for a custom type converter, it looks at each property for a TypeConverter attribute: // AlarmClockControl.cs partial class AlarmClockControl : ... { ... [TypeConverter(typeof(HandConverter))] public Hand HourHand {...} [TypeConverter(typeof(HandConverter))] public Hand MinuteHand {...} [TypeConverter(typeof(HandConverter))] public Hand SecondHand {...} ... } However, this code can be quite cumbersome. Sometimes it's simpler to decorate the type itself with the TypeConverter attribute: // Hand.cs [TypeConverter(typeof(HandConverter))] class Hand {...} // AlarmClockControl.cs partial class AlarmClockControl : ... { ... public Hand HourHand {...} public Hand MinuteHand {...} public Hand SecondHand {...} ... } Figure 11.20 shows the effect of the custom HandConverter type converter. Figure 11.20. HandConverter in Action (See Plate 17)
Expandable Object ConverterAlthough using the UI shown in Figure 11.20 is better than not being able to edit the property at all, there are still ways it can be improved. For example, put yourself in a developer's shoes; although it might be obvious what the first part of the property is, it's disappointing not to be able to pick the color from one of those pretty drop-down color pickers. And what is the second part of the property meant to be? Length? Width? Degrees? Thingamajigs? As an example of what you'd like to see, the Font type supports browsing and editing of its subproperties, as shown in Figure 11.21. Figure 11.21. Expanded Property Value
This ability to expand a property of a custom type makes it a lot easier to understand what the property represents and what sort of values you need to provide. To allow subproperty editing, you change the base type from TypeConverter to ExpandableObjectConverter (from the System.ComponentModel namespace): // HandConverter.cs class HandConverter : ExpandableObjectConverter {...} This trivial change supports both multivalue and nested property editing, as illustrated in Figure 11.22. Figure 11.22. HandConverter Derived from ExpandableObjectConverter
Although you don't have to write any code to make this property expandable, you need to fix an irksome problem: a delay in property updating. In expanded mode, a change to the parent property value (e.g., SecondHand) is automatically reflected in its child property values (e.g., Color and Width). This occurs because the parent property value refers to the design-time property instance, whereas its child property values refer directly to the design-time instance's properties, as shown in Figure 11.23. Figure 11.23. Relationship Between Parent and Child Properties and Design-Time Property Instance
When the parent property is edited, the Properties window calls HandConverter.ConvertFrom to convert the Properties window's string entry to a new SecondHand instance, and that results in a refresh of the Properties window. However, changing the child property values only changes the current instance's property values, rather than creating a new instance. Consequently, there isn't an immediate refresh of the parent property.
However, you can force the parent property value to be refreshed through the use of the NotifyParentProperty attribute. When a property type is a class with one or more other properties, such as SecondHand, you adorn each of the child properties of that class with the NotifyParentProperty attribute, passing true to its constructor: // Hand.cs [TypeConverter(typeof(HandConverter))] class Hand { ... [System.ComponentModel.NotifyParentProperty(true)] [Description("Sets the color of the clock Hand.")] public Color Color { get {...} set {...} } [System.ComponentModel.NotifyParentProperty(true)] [Description("Sets the width of the clock Hand.")] public int Width { get {...} set {...} } } TypeConverters also offer a mechanism by which you can force the creation of a new instance whenever instance property values change, and this is a great alternative in complex scenarios that require code to refresh a property. To implement refreshing of the parent property value, you override TypeDescriptor's GetCreateInstanceSupported and CreateInstance methods. The GetCreateInstanceSupported method returns a Boolean indicating whether this support is available and, if it is, calls CreateInstance to implement it: // HandConverter.cs class HandConverter : ExpandableObjectConverter { ... public override bool GetCreateInstanceSupported( ITypeDescriptorContext context) { // Always force a new instance return true; } public override object CreateInstance( ITypeDescriptorContext context, IDictionary propertyValues) { // Use the dictionary to create a new instance return new Hand((Color)propertyValues["Color"], (int)propertyValues["Width"]); } } If GetCreateInstanceSupported returns true, then CreateInstance is used to create a new instance whenever any of the subproperties of an expandable object are changed. The propertyValues argument to CreateInstance provides a set of name/value pairs for the current values of the object's subproperties, and you can use them to construct a new instance. Both the NotifyParentProperty attribute and TypeDescriptor techniques apply only to a single property; this keeps the parent property value synchronized with changes made to the child property values. Sometimes, however, you may need to refresh several properties when one property changes. In this case, the simplest approach is to use the RefreshProperties attribute to force the Properties window to update itself, retrieving new property values in the process. To control how the Properties window does that, if at all, you pass in one of three RefreshProperties enumeration values: namespace System.ComponentModel { enum RefreshProperties { None = 0, // Don't refresh at all All = 1, // Refresh property values for all properties in the // Properties window Repaint = 2 // Refresh property values for only those properties // that are visible in the Properties window } } To refresh all property values, you use the RefreshProperties attribute and RefreshProperties.All: [RefreshProperties(RefreshProperties.All)] public string SomeProperty { get {...} set {...} } Custom Type Code Serialization with TypeConvertersAlthough the Hand type now plays nicely with the Properties window, it doesn't yet play nicely with code serialization. In fact, at this point, its values are not being serialized to InitializeComponent at all. To enable serialization of properties of complex types, you must expose a public ShouldSerializePropertyName method that returns a Boolean: // AlarmClockControl.cs partial class AlarmClockControl : ... { ... public Hand HourHand {...} bool ShouldSerializeHourHand() { // Serialize only nondefault values return ((this.hourHand.Color != Color.Black) || (this.hourHand.Width != 1)); } ... } Internally, the Windows Forms Designer looks for a method named ShouldSerializePropertyName to ask whether the property should be serialized. From the Windows Forms Designer's point of view, it doesn't matter whether your ShouldSerializePropertyName is public or private, but choosing private removes it from client visibility. To programmatically implement the Properties window reset functionality, you use the ResetPropertyName method: // AlarmClockControl.cs partial class AlarmClockControl : ... { ... public Hand HourHand {...} bool ShouldSerializeHourHand() { // Serialize only nondefault values return ((this.hourHand.Color != Color.Black) || (this.hourHand.Width != 1)); } void ResetHourHand() { HourHand = new Hand(Color.Black, 1); // Refresh clock face this.Invalidate(); } ... } Implementing ShouldSerialize lets the design-time environment know whether the property should be serialized, but you also need to write custom code to assist in the generation of appropriate InitializeComponent code. Specifically, the Windows Forms Designer needs an instance descriptor, which provides the information needed to create an instance of a particular type. The code serializer gets an InstanceDescriptor object for a Hand by asking the Hand type converter: // HandConverter.cs class HandConverter : ExpandableObjectConverter { ... public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType) { // We can be converted to a string or an InstanceDescriptor if( destinationType == typeof(string) ) return true; if( destinationType == typeof(InstanceDescriptor) ) return true; return base.CanConvertTo(context, destinationType); } ... public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { // If source value is a Hand type if( value is Hand ) { // Convert to string if( destinationType == typeof(string) ) {...} // Convert to InstanceDescriptor if( (destinationType == typeof(InstanceDescriptor)) ) { Hand hand = (Hand)value; object[] properties = new object[2]; Type[] types = new Type[2]; // Color types[0] = typeof(Color); properties[0] = hand.Color; // Width types[1] = typeof(int); properties[1] = hand.Width; // Build constructor ConstructorInfo ci = typeof(Hand).GetConstructor(types); return new InstanceDescriptor(ci, properties); } } // Base ConvertTo if InstanceDescriptor not required return base.ConvertTo(context, culture, value, destinationType); } ... } To be useful, an instance descriptor requires two pieces of information. First, it needs to know what the constructor looks like. Second, it needs to know which property values should be used if the object is instantiated. The former is described by the ConstructorInfo type, and the latter is simply an array of values, which should be in constructor parameter order. After the component is rebuilt, and assuming that ShouldSerialize PropertyName permits, all Hand type properties are serialized using the information provided by the HandConverter-provided InstanceDescriptor: // AlarmClockControlHostForm.Designer.cs partial class AlarmClockControlHostForm { ... void InitializeComponent() { ... this.alarmClockControl.SecondHand = new AlarmClockControlLibrary.Hand( System.Drawing.Color.LimeGreen, 7); ... } ... } Type converters provide all kinds of help for the Properties window and the Windows Forms Designer to display, convert, and serialize properties of custom types for components that use such properties. |