You are on page 1of 35

Chapter 8.

Property Grids
Many applications need to present sets of information that can be edited. The data could be patient details in a healthcare system, shape attributes in a drawing program, control properties in a form designer, or any number of other kinds of information. Windows Forms provides a control that makes presenting and editing such data easy, while allowing great flexibility in the way in which information is presentedthe PropertyGrid control. Visual Studio .NET itself uses the PropertyGrid control to present properties for all controls and other components in the Forms Designer. This means that even if you don't plan to use a PropertyGrid directly in your own applications, it is helpful to have a good understanding of how it works so you may control the way your components' properties are presented. Visual Studio .NET lets you customize the appearance of the property pages for your components using the techniques described here. In this chapter, we will start by looking at how to display the properties of a simple object in a PropertyGrid. Then, we will see how to enable editing of custom types by using type converters. Finally, we will see how to add our own custom property editing user interfaces for when a text-based representation is insufficient.

8.1 Displaying Simple Objects


The PropertyGrid makes it remarkably easy to provide an interface for editing the properties of an object. It uses the CLR's reflection facility to discover what properties are available and presents them automatically. This means that it can be used on simple classes such as those shown in Examples Example 8-1 and Example 8-2. Example 8-1. A simple class using C#
public class CustomerDetails { private string firstName, lastName, address; private DateTime dob; public string FirstName { get { return firstName; } set { firstName = value; } } public string LastName { get { return lastName; } set { lastName = value; } } public DateTime DateOfBirth { get { return dob; }

set { dob = value; }

public string Address { get { return address; } set { address = value; } } }

Example 8-2. A simple class using VB


Public Class CustomerDetails Private sFirstName, sLastName, sAddress As String Private dob As Date Public Property FirstName As String Get Return sFirstName End Get Set sFirstName = Value End Set End Property Public Property LastName As String Get Return sLastName End Get Set sLastName = value End Set End Property Public Property DateOfBirth As Date Get Return dob End Get Set dob = Value End Set End Property Public Property Address As String Get Return sAddress End Get Set sAddress = Value End Set End Property End Class

Displaying an instance of this class in a PropertyGrid is trivial. Simply drag a PropertyGrid control onto a form, and then in that form's constructor, create an instance of the object you wish to display, and pass it to the grid by setting its SelectedObject property, as shown in Example 8-3. The PropertyGrid control may not appear on your toolbox. To add this control to the toolbox, right-click on the toolbox and select Customize Toolbox.... Select the .NET Framework Components tab and make sure that the checkbox for the Property Grid control is checked.

Example 8-3. Displaying an object in a PropertyGrid


// C# code CustomerDetails cd = new CustomerDetails(); cd.FirstName = "John"; cd.LastName = "D'Oh"; cd.Address = "742, Evergreen Terrace, Springfield"; cd.DateOfBirth = new DateTime(1956, 5, 12);
propertyGrid.SelectedObject = cd;

' VB code Dim cd As New CustomerDetails() cd.FirstName = "John" cd.LastName = "D'Oh" cd.Address = "742, Evergreen Terrace, Springfield" cd.DateOfBirth = #5/12/1956#
PropertyGrid1.SelectedObject = cd

The property grid will then examine the object and discover that it has four public properties, which it will present for editing as shown in Figure 8-1. Figure 8-1. A simple object in a PropertyGrid

The grid is shown here with its default settingsthe toolbar is present along the top, the properties are sorted by category, and the description pane is visible. For the object being displayed, none of this is particularly useful, because the properties are not categorized (which is why everything appears under Misc), and they do not have descriptions. These

unused features are easy enough to switch offsetting the ToolbarVisible and HelpVisible properties to false will remove the toolbar and description pane, while setting the PropertySort property to PropertySort.Alphabetical will prevent the grid from trying to display category names. However, descriptions and categorizations can be very useful, so it would be better to modify our CustomerDetails class to make use of them. The way we supply the PropertyGrid with category and description information is to annotate our properties with custom attributes. In fact, we use the same Category and Description attributes that we would when annotating a control's properties for Visual Studio .NET's benefit, as described in Chapter 5. This should come as no surprise, because the Forms Designer uses a PropertyGrid to display a control's properties. So if we add relevant attributes, as shown in Example 8-4, the PropertyGrid will use them to display the appropriate categories and descriptions for our object. Example 8-4. Annotating a property
// C# code [Category("Name")] [Description("The customer's first name")] public string FirstName { . . . As before ' VB code <Category("Name"), _ Description("The customer's first name")> _ Public Property FirstName As String . . . As before

These attributes are all defined in the System.ComponentModel namespace, so make sure you have a using (in C#) or Imports (in VB) statement at the top of your file to bring that namespace into scope. Figure 8-2 shows how the PropertyGrid uses these attributes when presenting our object's properties. Figure 8-2. Properties shown with categories and descriptions

As with components displayed in Visual Studio .NET, any properties marked with the [Browsable(false)] attribute will not be displayed by default. But this is not the only

way of filtering which properties the grid will displaythe PropertyGrid control provides a property called BrowsableAttributes. This can be set to an AttributeCollection containing a list of attributes that must be present on a property for it to be displayed. The code shown in Example 8-5 will cause only those properties belonging to the Name category to be shown. (Note that the grid will only show those properties that have all the attributes specified. So if you were to build a list with two different category attributes in it, you would end up with nothing in the grid.) You don't have to use category attributes in this listany kind of attribute can be specified, so if you want to filter properties on some other criteria, you could define your own custom attribute class. Example 8-5. Filtering on category
// C# code propertyGrid.BrowsableAttributes = new AttributeCollection( new Attribute[] { new CategoryAttribute("Name") }); ' VB code PropertyGrid1.BrowsableAttributes = New AttributeCollection( _ New Attribute() {New CategoryAttribute("Name")})

The ability to supply category names and descriptions is a powerful usability enhancement. However, the problem with the examples we have seen so far is that they hardcode strings into the source code. This is bad practice because it makes it difficult to display localized versions of the strings when your software runs in other locales. You can avoid this by making localizable versions of these attributes.

8.1.1 Localizable Descriptions and Categories


The strings used for the Category and Description attributes are intended to be read by end users. Unfortunately, they require you to hardcode such strings into your source code, which makes it very awkward to create localized versions of your application. As we saw in Chapter 3, you should retrieve all culture-specific properties from a ResourceManager to allow the appropriate values to be determined at runtime. This allows resources for new cultures to be added as satellite assemblies without requiring any changes to your code. Unfortunately, and somewhat surprisingly, the Category and Description attributes provide no direct support for localization. We must derive our own culture-aware versions of these classes if we are to support multiple cultures. The classes that define these attributes are designed to be inherited from for localization purposes, although curiously, they prescribe different techniques. With the Category attribute, there is a GetLocalizedString method that we overload in order to supply a localized version. Examples Example 8-6 and Example 8-7 show how to do this. Example 8-6. A localizable category attribute using C#
[AttributeUsage(AttributeTargets.All)] public class LocalizableCategoryAttribute : CategoryAttribute { private Type t;

public LocalizableCategoryAttribute(string n, Type resBase) : base (n) { t = resBase; } protected override string GetLocalizedString(string value) { ResourceManager rm = new ResourceManager(t); string tx = rm.GetString(value); if (tx != null) return tx; } } return base.GetLocalizedString(value);

Example 8-7. A localizable category attribute using VB


<AttributeUsage(AttributeTargets.All)> _ Public Class LocalizableCategoryAttribute Inherits CategoryAttribute Private t As Type Public Sub New(n As String, resBase As Type) MyBase.New(n) t = resBase End Sub Protected Overrides Function GetLocalizedString(value As String) As String Dim rm As New ResourceManager(t) Dim tx As String = rm.GetString(value) If tx <> Nothing Then Return tx Return MyBase.GetLocalizedString(value) End Function End Class

Note that when deriving from an existing attribute, you must redeclare the valid target types, hence, the AttributeUsage attribute. (The Category attribute declares itself to be valid for all target types, so we follow suit.) The overridden GetLocalizedString method just uses a ResourceManager to look up the localized version of the string. If this fails, it defers to the base class (which will just return the original string). To create a ResourceManager, we need to supply enough information for the framework to locate the appropriate resource file. The standard way of doing this is to use a Type objectresources are typically associated with a type. In Visual Studio .NET the way you manage this is to name the resource file after the class it is to be associated with. So to use localizable resources on our CustomerDetails class, we would add a new Assembly

Resource File called CustomerDetails.resx. Having done this, we can then use the localizable form of this attribute on our class's properties:
// C# code [LocalizableCategory("Name", typeof (CustomerDetails))] public string FirstName { get { return firstName; } set { firstName = value; } } ' VB code <LocalizableCategory("Name", GetType(CustomerDetails))> _ Public Property FirstName As String Get Return sFirstName End Get Set sFirstName = Value End Set End Property

So when the PropertyGrid control attempts to use our modified CustomerDetails object, it will look for the Category attribute as usual, but it will actually get our LocalizableCategory instead. When the grid asks the attribute for the category name, our GetLocalizedString method will be called. This will ask the ResourceManager to find a definition for the string that is appropriate to the current locale. If the ResourceManager cannot find one, our attribute will just return the unlocalized string. To see this in action, let us add a culture-specific resource file to our project, as shown in Figure 8-3. Figure 8-3. A culture-specific resource file

Figure 8-3 shows a .resx file as presented by Visual Studio .NET. This particular file is called CustomerDetails.fr-FR.resx. The fr-FR part indicates that this file contains French resources. This will cause Visual Studio .NET to compile it into a so-called satellite assembly (a culture-specific resource-only assembly) and place it in the fr-FR subdirectory. The first fr in the resource filename indicates the language: French. The second FR indicates the region: France. Region and language are specified independently, because either on its own is not enough French is spoken in many countries, many of which also speak other languages. For example, Canada (fr-CA) or Belgium (fr-BE).

If we run our application in a French locale, when the property grid asks our LocalizableCategory for the category name, our GetLocalizedString method will pass the hardcoded name (e.g., Demographics) to the GetString method of the ResourceManager. The resource manager will look for a satellite assembly in the fr-FR subdirectory because the current culture is French. It will find the satellite assembly containing the resource file shown in Figure 8-3, and will look up the entry whose name is Demographics, and return its value, Dmographiques. Consequently, when the property grid appears, the category names appear in French, not in English, as shown in Figure 8-4. Figure 8-4. Translated category names

We are not done yetthe description and property names are still in English. The description can be fixed in much the same way that categories werewe define our own custom attribute that derives from the Description attribute. As before, the Description attribute was designed to be derived from, so this is relatively straightforward, although for some reason the prescribed way of supporting localization is somewhat differentwe are expected to translate the string just once, and store it in a protected property called DescriptionValue. Examples Example 8-8 and Example 8-9 show an implementation of this. Example 8-8. A localizable description attribute using C#
[AttributeUsage(AttributeTargets.All)] public class LocalizableDescriptionAttribute : DescriptionAttribute { private Type t; public LocalizableDescriptionAttribute(string name, Type resBase) : base(name) { t = resBase; } private bool localized = false; public override string Description { get { if (!localized) { localized = true; ResourceManager rm = new ResourceManager(t);

} return base.Description; } }

string tx = rm.GetString(DescriptionValue); if (tx != null) DescriptionValue = tx;

Example 8-9. A localizable description attribute using VB


<AttributeUsage(AttributeTargets.All)> _ Public Class LocalizableDescriptionAttribute Inherits DescriptionAttribute Private t As Type Private localized As Boolean = False Public Sub New(ByVal name As String, _ ByVal resBase As Type) MyBase.New(name) t = resBase End Sub Public Overrides ReadOnly Property Description() As String Get If Not localized Then localized = True Dim rm As New ResourceManager(t) Dim tx As String = rm.GetString(DescriptionValue) If Not tx Is Nothing Then DescriptionValue = tx End If Return MyBase.Description End Get End Property End Class

This conforms to the idiom required by the Description attribute, and it works in a slightly curious fashion. In our override of the Description property's get method, we are required to read the DescriptionValue property, translate it, and then write back the translated value. We must then defer to the base class's get implementation, which just returns the value of DescriptionValue. This is a somewhat roundabout way of doing things, but it is what the documentation for DescriptionAttribute instructs us to do. Apart from the slightly peculiar way in which the overridden Description property works, this class uses the same technique as we used for our localizable categoryit relies on a ResourceManager to find the appropriate string for the current culture. However, you will probably want to use this attribute slightly differently, as Example 8-10 shows. Example 8-10. Using a localizable description attribute
// C# [LocalizableDescription("LastName.Description", typeof(CustomerDetails))]

public string LastName { . . . as before ' VB <LocalizableDescription("LastName.Description", _ GetType(CustomerDetails))> _ Public Property LastName() As String . . . as before

Example 8-10 shows the LocalizableDescription attribute in use. Notice that the string being supplied to the attribute (LastName.Description) is not the full description. This is because, for non-English cultures, this string will be used to look up the translated string. Using a full English sentence as a key to look up information is error prone (not to mention inefficient). There is nothing stopping you from using the full sentence in the attribute, it is just that you are more likely to run into problems. (However, it does have the advantage that you don't need to supply an entry for the string in the default resources.) If you use the technique shown in Example 8-10 you will obviously need to supply entries for these strings in your neutral resources (i.e., the resources compiled into the main executable, not a satellite assembly) so that the correct strings appear for your default culture. As Figure 8-5 shows, both the category names and the descriptions are now localized. However, we are still not quite done. The property names are still displayed in English. To change this, we will need to use something called a TypeConverter, which enables us to modify the way in which a PropertyGrid presents properties. In fact, we can do far more with a TypeConverter than just changing the displayed name of the property. Figure 8-5. Translated categories and descriptions

8.2 Type Conversion


The PropertyGrid is able to edit many different kinds of data, and can provide specialpurpose user interfaces for certain types. For example, the CustomerDetails class shown earlier has a DateTime or Date field, and the PropertyGrid can display a date picker control when you edit this control. It supports all the built-in types, and all the types used on common properties on controls. It is also possible to extend its capabilities so that it can edit new types.

The PropertyGrid turns out not to have a long list of types that it knows how to display and edit. The control itself knows nothing about, say, the DateTime or Color types, and yet it is still able to present them for editing. This is because it has a very flexible open architecture that allows any type to make itself editable. A type can provide various levels of support for the PropertyGrid, even going as far as supplying a special-purpose user interface for editing that type (like the pickers that appear for Color and ContentAlignment). We will see how to do that later, but for many types, simple text editing will suffice. So at the bare minimum, our type must support conversion to and from textits value will be converted to text when it is first displayed in the grid. If the user changes that text, the new string must be converted back to an instance of our type for the edit to take effect. We will now introduce a custom type to the CustomerDetails example and then add support for basic type conversion to and from a string. Rather than storing the customer name as first and last name strings, we will define a separate CustomerName type, as shown in Examples Example 8-11 and Example 8-12. The CustomerDetails class defined in Examples Example 8-1 and Example 8-2 will be modified to expose a single property Name of type CustomerName instead of the original FirstName and LastName properties. This CustomerName class is shown in Examples Example 8-11 and Example 8-12. Example 8-11. Custom type to hold name written in C#
public class CustomerName { private string firstName, lastName; public string FirstName { get { return firstName; } set { firstName = value; } } public string LastName { get { return lastName; } set { lastName = value; } } }

Example 8-12. Custom type to hold name written in VB


Public Class CustomerName Private sFirstName, sLastName As String Public Property FirstName() As String Get Return sFirstName End Get Set sFirstName = Value End Set End Property

Public Property LastName() As STring Get Return sLastName End Get Set sLastName = Value End Set End Property End Class

So far we have not provided any support for the benefit of PropertyGrid. Consequently, when we display the modified CustomerDetails in the grid, its Name field is not especially helpful. Figure 8-6 shows how the property will be displayed. Figure 8-6. An unsupported type in a PropertyGrid The field is displayed, but not with any useful informationjust the name of the field's type. The reason that the type name has appeared is that PropertyGrid just calls the ToString method on types it doesn't know how to deal with, and the default implementation of ToString is to return the type name. We can override this in the CustomerName class easily enough to provide some more useful information:
// C# code public override string ToString() { return string.Format("{0}, {1}", LastName, FirstName); } ' VB code Public Overrides Function ToString() As String Return String.Format("{0}, {1}", LastName, FirstName) End Function

This improves matters slightlyas you can see in Figure 8-7, the grid now shows a meaningful representation of the property's value. But it is grayed out, and the PropertyGrid will not let the user edit the text. This is because although the PropertyGrid was able to convert our type to a string by calling ToString, it does not know how to convert a string back to an instance of our type. It cannot allow the value to be edited because it has no way of writing the edited value back into our object. Figure 8-7. Using ToString in a PropertyGrid To allow our CustomerName type to be edited in a PropertyGrid, we will need to provide a conversion facility allowing the property to be set using a string. We do this by writing a class that derives from TypeConverter. Examples Example 8-13 and Example 8-14 show how to do this for our type. It overrides two methods. First, it overrides CanConvertFrom this method can be called to find out whether a particular source type can be converted

into our CustomerName type. The PropertyGrid control will call this method to find out whether it will be able to convert from a string to a CustomerNamevalues are typically edited as text on a property grid. So we test the sourceType parameter and return true if it we are being asked to convert to a string. Example 8-13. A custom TypeConverter using C#
public class CustomerNameConverter : TypeConverter { public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); } public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value) { if (value == null) return null; // Make sure this is a string string sval = value as string; if (sval == null) throw new NotSupportedException("Unsupported type"); // If comma is present, treat this as "Last, First" string[] names = sval.Split(','); if (names.Length == 2) { CustomerName name = new CustomerName(); name.LastName = names[0].Trim(); name.FirstName = names[1].Trim(); return name; } else if (names.Length == 1) { // No comma, must be "First Last" names = sval.Split(' '); if (names.Length == 2) { CustomerName name = new CustomerName(); name.FirstName = names[0].Trim(); name.LastName = names[1].Trim(); return name; } } // Unable to make sense of the string throw new NotSupportedException("Invalid format"); } }

Example 8-14. A custom TypeConverter using VB

Public Class CustomerNameConverter Inherits TypeConverter Public Overloads Overrides Function CanConvertFrom( _ context As ITypeDescriptorContext, _ sourceType As Type) _ As Boolean Return sourceType Is GetType(String) End Function Public Overloads Overrides Function ConvertFrom( _ context As ITypeDescriptorContext, _ culture As CultureInfo, _ value As Object) _ As Object If value Is Nothing Then Return Nothing ' Make sure this is a string Dim sVal As String If TypeName(value) <> "String" Then Throw New NotSupportedException("Unsupported type") Else sVal = DirectCast(value, String) End If ' If comma is present, treat this as "Last, First" Dim names() As String = sval.Split(","c) If names.Length = 2 Then Dim name As New CustomerName() name.LastName = names(0).Trim() name.FirstName = names(1).Trim() Return name Else If names.Length = 1 Then ' No comma, must be "First Last" names = sval.Split(" "c) If names.Length = 2 Then Dim name As New CustomerName() name.FirstName = names(0).Trim() name.LastName = names(1).Trim() Return name End If End If ' Unable to make sense of the string Throw New NotSupportedException("Invalid format") End Function End Class

The second method that we override is the ConvertFrom method. This is where we do the conversion, parsing the string to create a CustomerName. (This particular example allows the name to be passed in two formats: "First Last" and "Last, First." It uses the presence or absence of a comma to work out which format is being used.) There are two other methods we might consider overriding here: CanConvertTo and ConvertTo. These perform the reverse transformation; for CustomerNameConverter, this

means converting from a CustomerName to a string. In this case, there is no need to override thesethe implementation supplied by the base class, TypeConverter, already handles conversion to a string by calling ToString. Because we provided a suitable ToString method on CustomerName, we don't need to add anything here. But if you wanted to support editing of a type whose ToString method returned an inappropriate value, you could bypass it by overriding ConvertTo in the TypeConverter. (Because the default CanConvertTo method always returns true for string, you do not need to override it when providing custom string conversion in ConvertTo. You would only need to override CanConvertTo if you decide to support other conversions.) This TypeConverter now provides the conversion facilities required by the PropertyGrid. The only remaining question is this: how does the PropertyGrid know that it should use this CustomerNameConverter when editing a CustomerName? It won't just guess from the class namesit needs a more positive hint than that. The answer is that when the PropertyGrid encounters a data type that it doesn't intrinsically know how to deal with, it will look to see if that type has the TypeConverter attribute. So we can use this to annotate our CustomerName class, as shown in Example 8-15. Example 8-15. Associating a type with its TypeConverter
// C# code
[TypeConverter(typeof(CustomerNameConverter))]

public class CustomerName { . . . as before ' VB code <TypeConverter(GetType(CustomerNameConverter))> _ Public Class CustomerName . . . as before

If the TypeConverter attribute is present, the PropertyGrid will use the converter that it specifies to do all conversion to and from strings. With this attribute in place, our Name field becomes editable. When we make a change to the Name field, the PropertyGrid will pass the edited text to the ConvertFrom method of the CustomerNameConverter, which will parse the string and build a new CustomerName based on its contents. In other words, our property can now be edited like any other. The PropertyGrid control will search for the TypeConverter attribute both on the definition of the type being edited, and also on the property. This can be useful for two reasons. If your class has a property of a type whose definition you don't control, that type may well not have an associated converter. This is not a problem, because you can write your own converter and just specify it on the property where you use the type in question, as Example 8-16 shows. Similarly, you might be using a type you didn't write that does have an associated converter, but for some reason you need to use a different one (e.g., to deal with localization issues). The PropertyGrid will check for the TypeConverter attribute on the property first, so you can replace a type's default converter with your own.

Example 8-16. Specifying a converter on a property


// C# code public class CustomerDetails { private CustomerName name; private string address; private DateTime dob; [TypeConverter(typeof(CustomerNameConverter))] public CustomerName Name { get { return name; } set { name = value; } } } . . . as before

' VB code Public Class CustomerDetails Private oName As New CustomerName() Private sAddress As String Private dob As Date <TypeConverter(GetType(CustomerNameConverter))> _ Public Property Name() As CustomerName Get Return oName End Get Set(ByVal Value As CustomerName) oName = Value End Set End Property . . . . as before

Being able to edit a CustomerName is good, but we can do better. Windows Forms often uses similar propertiesjust as CustomerName has its FirstName and LastName properties, the Size property of a form also has some subproperties, Width and Height. And while you can edit a control's size by typing in the width and height as a single string, it also allows the Size property to be expanded, with the Width and Height properties displayed as children. We can do exactly the same thingPropertyGrid lets any property display such nested properties. All we need to do is supply an appropriate TypeConverter.

8.2.1 Nested Properties


A nested property is any property of an object that itself is a property of some other object. For example, the Name property on our CustomerDetails type has two nested properties, FirstName and LastName. The property grid is able to make properties such as Name expandablea plus sign can be added, and when clicked, it will show the nested properties in the grid.

This facility is enabled by using an appropriate TypeConverter. The simplest approach is to use the converter supplied by the system for this purpose ExpandableObjectConverter:
[TypeConverter(typeof(ExpandableObjectConverter))]

public class CustomerName { . . . as before

This will use reflection to discover what properties are available, and supply these to the PropertyGrid, enabling it to display them as shown in Figure 8-8. Figure 8-8. Nested properties in a PropertyGrid

There are two problems with this. First, because we are no longer using the purpose-built CustomerNameConverter (we replaced that with ExpandableObjectConverter) the PropertyGrid no longer has any way of converting text back to a CustomerNamethe ExpandableObjectConverter has no knowledge of the parsing rules we are using for the conversion. This means that the text against the Name field in the grid is no longer editable; the Name property can only be changed by editing its nested properties. Second, the PropertyGrid will not update the text next to the Name field when you edit either the FirstName or LastName fields because it doesn't know that the fields are related. The simple way to solve both of these problems is to change the CustomerName class's ToString method to return an empty string. This makes the fact that the Name field cannot be edited less obtrusive, because there will be nothing there to edit. And if the field is always empty, it no longer matters that it isn't updated when the nested properties change. However, we can do better than this. Properties of type Size and Position allow their values to be edited either as a whole or through their nested properties. We can provide the same flexibility with our own types. To make sure that the main property value is refreshed every time one of its nested properties changes, simply mark all nested properties with the RefreshProperties attribute:
[RefreshProperties(RefreshProperties.Repaint)]

public string FirstName { . . . as before

This will cause the PropertyGrid to check the parent property's value after the nested property changes, and update the display if necessary. But having done this, we still need to arrange for the parent property (Name in this case) to be editable directly. What we really want is a type converter that has the nested object facility of the ExpandableObjectConverter, but that also has the parsing logic of our CustomerNameConverter. This is easy to achievesimply modify the

CustomerNameConverter so that it inherits from ExpandableObjectConverter, and change the TypeConverter attribute on the CustomerName class back to refer to CustomerNameConverter, as in Example 8-15.

Although our nested properties now appear to be behaving in the same way as Size and similar standard properties, there is a more subtle difference: the framework tends to use value types for such properties, but our CustomerName is a class. Unfortunately, if you try to change CustomerName to be a value type, you will find that this example stops working. This is because value types require special treatment when used in a PropertyGrid. 8.2.1.1 Value types Some properties use value typesthere are several examples in the Control class alone (Location, Size, and Bounds, for example; their respective types are Point, Size, and Rectangle, which are all value types). But these are slightly trickier for the PropertyGrid to usenested properties don't work without special treatment. This is mainly because to change a value type property, you must update the whole property. (If you retrieve the Size property, and then change the Width, you will simply be changing the Width in your local copy of the Size. You must then write this modified Size back to update the property.) Suppose that CustomerName were a value type, and that we wished to display its nested properties in a PropertyGrid as before. If the user modifies the FirstName nested property of the Name, the grid somehow has to apply that change back to the property. It cannot modify the FirstName in situ its only option is to build a CustomerName with the correct value and assign that to the Name. But how is it supposed to know how to create a CustomerName? Again, the TypeConverter comes to the rescueit has two methods we can override to provide the PropertyGrid with the facility that it requires. Examples Example 8-17 and Example 8-18 illustrate this.
[1]

This is for exactly the same reason that you can't do it in code. If you tried to write customer.Name.FirstName = "Fred"; the compiler would complain that this is not possible because the Name property uses a value type.
[1]

Example 8-17. Supporting value types in a TypeConverter using C#


public override bool GetCreateInstanceSupported( ITypeDescriptorContext context) { return true; } public override object CreateInstance( ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) { CustomerName n = new CustomerName(); n.FirstName = (string) propertyValues["FirstName"]; n.LastName = (string) propertyValues["LastName"]; return n; }

Example 8-18. Supporting value types in a TypeConverter using VB


Public Overrides Overloads Function GetCreateInstanceSupported( _ context As ITypeDescriptorContext) _ As Boolean Return True End Function Public Overrides Overloads Function CreateInstance( _ context As ITypeDescriptorContext, _ propertyValues As System.Collections.IDictionary) _ As Object Dim n As New CustomerName() n.FirstName = CStr(propertyValues("FirstName")) n.LastName = CStr(propertyValues("LastName")) Return n End Function

Examples Example 8-17 and Example 8-18 illustrate how to provide support for a value type in a TypeConverter. The PropertyGrid will call the GetCreateInstanceSupported method to find out whether our converter provides a value creation facility. We simply return true to indicate that we do. We then supply the CreateInstance method, which it will call when it needs us to create a new value. For example, if the user edits the FirstName nested property, the PropertyGrid will call CreateInstance. It will pass in an IDictionary containing the modified FirstName value and the original LastName value as strings. We convert these strings back into a complete CustomerName that the PropertyGrid can then use to set the Name property on the CustomerDetails object.
[2] [2]

This is just a collection of (name, value) pairs.

You do not need to use the RefreshProperties attribute on the properties of value types. This is because the parent property is always updated in its entirety whenever any of the nested properties change, so the grid always refreshes it. The ExpandableObjectConverter type provides a convenient way of allowing nested properties to be edited, but there is nothing magic about itit just overrides a couple of methods of the TypeConverter class that are there to support nested properties. It can be useful to do this yourself if you need more control over the way in which properties are presented. For example, this is the only way to make your property names localizable. So we will now look in a little more detail at the mechanism on which nested properties are based.

8.2.2 Property Descriptors


The PropertyGrid control always uses the objects that it presents through a level of indirection. Instead of accessing properties directly (or as directly as is possible using reflection), it always goes through a PropertyDescriptor. You can control exactly what descriptors the PropertyGrid gets to see, or even create your own descriptors, simply by

overriding the appropriate methods of your class's TypeConverter. This allows you to customize how the PropertyGrid sees your type, and therefore to control how it appears in the grid. Whenever it displays any object (whether it is the main object or an object supplying nested properties), the PropertyGrid first obtains a set of PropertyDescriptor objects to determine what needs to be shown in the grid. It will attempt to get this list from the object's associated TypeConverter (if it has one). First, it will call the converter's GetPropertiesSupported method to find out whether this particular TypeConverter is able to supply property descriptors. If the GetPropertiesSupported method returns true, it will then call GetProperties to retrieve a list of PropertyDescriptor objects. The grid will display whatever properties are in this list, regardless of what properties the object might really have. (If it is unable to get these descriptors from a converter, it falls back to calling the TypeDescriptor class's GetProperties method for the main object in the grid, which builds the list using reflection; for nested objects, it falls back to not displaying any properties at all.) This means that by supplying a type converter and overriding these two methods, we have complete control over what properties the grid will display. We can filter the properties, modify how they will appear, or even fake them up entirely. If we create a PropertyDescriptor for which there is no real underlying property, the grid will never know, because it never interacts with properties directlyit always goes through a PropertyDescriptor. Building fake descriptors is somewhat harder than filtering because this requires you to write your own class that inherits from PropertyDescriptor. This is not completely trivial, because PropertyDescriptor has many abstract methods. However, TypeConverter provides a nested class, SimplePropertyDescriptor, that makes it much easier. It derives from PropertyDescriptor for you and provides implementations for most of the methods. If you plan to create your own property descriptors, it is usually easiest to use SimplePropertyDescriptor as a base class. Unfortunately, Visual Basic .NET currently has a limitation that prevents it from using this class. SimplePropertyDescriptor is a defined as a protected nested class. According to the .NET type system rules, this means that the only way to define a class derived from SimplePropertyDescriptor is to make that derived class a nested class inside a class that derives from TypeConverter. This works fine in C#, but Visual Basic unfortunately does not support this, due to a bug in the compiler. Until this bug is fixed, there is no way of using the SimplePropertyDescriptor class in Visual Basic .NET. Consequently, the examples in this section will be in C# only.

The ExpandableObjectConverter just builds a list of property descriptors for whichever object it is being asked to represent. This is trivial, because the TypeDescriptor class will do this for you. Example 8-19 shows a custom TypeConverter that is exactly equivalent to the ExpandableObjectConverter. Example 8-19. Do-it-yourself ExpandableObjectConverter
public class MyExpandableObjectConverter : TypeConverter { public override bool GetPropertiesSupported( ITypeDescriptorContext context) { return true; } public override PropertyDescriptorCollection GetProperties( ITypeDescriptorContext context, object value, Attribute[] attributes) { return TypeDescriptor.GetProperties(value, attributes, true); } }

This is not especially useful as it standsyou might as well use the built-in ExpandableObjectConverter class. However, not only is it interesting to see how easy it is to provide property descriptors, it can also act as a useful starting point. It is fairly easy to modify this class to build a TypeConverter that provides filtered views by removing items from the collection returned by the TypeDescriptor class. For example, we can use this facility to complete what we started earlier: we can write a TypeConverter that makes property names localizable. 8.2.2.1 Localization with property descriptors Earlier in this chapter, we saw how to create localizable versions of the Category and Description attributes. This enabled the category names and property descriptions to be shown in the appropriate language for the current culture. We will now finish the job by writing a TypeConverter that can localize the property names displayed in the grid. This seems as though it might be a hard problemafter all, the names of a class's properties are part of the source code and are not easily modifiable through the normal localization techniques. Fortunately, as we have just seen, the PropertyGrid does not access properties directlyit always goes through a level of indirection in the form of a PropertyDescriptor. All we need to do is provide a TypeConverter that supplies PropertyDescriptor objects with the names we want. The PropertyDescriptor class was designed with this kind of thing in mind, because it supports two different names for any property. The descriptor's Name property is the real

name, i.e., the name in the source code. But is also has a DisplayName property, which is the name that is to be displayed in the user interface whenever this property is shown. The PropertyGrid always uses the DisplayName, so all we need to do is make sure that it contains the localized version of the property name. Although the framework supplies some classes that derive from PropertyDescriptor, none of the concrete ones allows the DisplayName to be different from the Name. This means we will have to write our own. Fortunately, we are writing one as part of a TypeConverterthis is good because TypeConverter provides a useful abstract base class for writing your own PropertyDescriptor, called TypeConverter.SimplePropertyDescriptor. This does most of the work we require, so we only need to write a small amount of code to build a concrete class derived from PropertyDescriptor that meets our needs.
TypeConverter.SimplePropertyDescriptor is a nested class of TypeConverter, and it is marked as protected. This means that a

class that derives from


TypeConverter.SimplePropertyDescriptor must be a nested classes defined inside a class derived from TypeConverter.

Before we look at the code, we will consider how our localizing TypeConverter will be used in practice. We want it to be simple, having as little impact as possible on code that uses it. This complicates the implementation a little, but this converter only has to be written once, whereas the client code will be written everywhere that our converter is used, so it makes sense to complicate the converter to simplify its use. If we call our custom converter LocalizableTypeConverter, client code will look like this:
[TypeConverter(typeof(LocalizableTypeConverter))]

public class CustomerDetails { public CustomerName Name { . . . } public DateTime DateOfBirth { . . . } . . . }

In other words, the impact is no worse than supporting any other TypeConverter. There is one snag with thisbecause our TypeConverter will be localizing strings, it will need to create a ResourceManager. In order to create a resource manager, we need access to a Type object (or at least an Assembly). Fortunately, our TypeConverter will be able to discover the type of the class it has been attached to, and it can use that as its resource source. So in this case, it would use CustomerDetails. While this makes for a minimum of code, it does rather increase the number of .resx files you will need in your projectit will require a resource file for every class that uses this converter. So we will therefore define an optional custom attribute that allows a different type to be specified as the basis

for resources. Example 8-20 shows a type that uses this attribute to share a resource file with the CustomerDetails class. Example 8-20. Specifying a type for resource location
[TypeConverter(typeof(LocalizableTypeConverter))] [LocalizationResourceType(typeof(CustomerDetails))] public class CustomerName { . . . as before

These attributes are all that we will require the client code to use. Our localizing type converter will use the real names of the properties to look up the localized names in the resource manager. So simply adding entries in the culture-specific resource file mapping, say, Name to Nom, will be all that is required to localize the property names. Let us look at the code for the LocalizableTypeConverter and associated classes. Rather than presenting all the code in one go, we will look at it one piece at a time. Don't worry there are no missing pieces. First is the LocalizationResourceType attribute, which is shown in Example 8-21. Example 8-21. The LocalizationResourceType attribute
[AttributeUsage(AttributeTargets.All)] public class LocalizationResourceTypeAttribute : Attribute { private Type t; public LocalizationResourceTypeAttribute(Type resBase) { t = resBase; } public Type ResBase { get { return t; } } }

This is a straightforward custom attribute that holds a Type object for the benefit of the TypeConverter. Example 8-20 shows how this attribute will be used. This is just a perfectly normal and not very exciting custom attribute class, so we will move on to the converter's class declaration and its one field:
public class LocalizableTypeConverter : TypeConverter { private Type resBase = null;

Our class inherits from TypeConverter, because it is a type converter. The resBase field is used to hold the Type object for the class that will be used to initialize a ResourceManager. This will determine the name of the resource file that will contain the localized versions of the names. Next is the first override:
public override bool GetPropertiesSupported( ITypeDescriptorContext context)

{ }

return true;

Here we are simply indicating that our TypeConverter will supply PropertyDescriptor objects. The whole purpose of this class is to supply the PropertyGrid with appropriately tweaked descriptors, but it will only ask us for descriptors if we return true from this method, as we did in Example 8-19. Next is the GetProperties method, where we create our descriptors:
public override PropertyDescriptorCollection GetProperties( ITypeDescriptorContext context, object value, Attribute[] attributes) { EnsureAttrsRead(value); PropertyDescriptorCollection pdc;
pdc = TypeDescriptor.GetProperties(value, attributes, true);

PropertyDescriptor[] props = new PropertyDescriptor[pdc.Count]; for (int i = 0; i < pdc.Count; ++i) { Attribute[] attrs = new Attribute[pdc[i].Attributes.Count]; pdc[i].Attributes.CopyTo(attrs, 0); props[i] = new LocalizablePropertyDescriptor(resBase,
pdc[i], attrs);

} PropertyDescriptorCollection pdcOut = new PropertyDescriptorCollection(props); return pdcOut; }

The EnsureAttrsRead method, shown below, makes sure that we have checked for the presence of the LocalizationResourceType attribute before proceeding. We use the TypeDescriptor class to provide us with a complete set of nonlocalized PropertyDescriptor objects. We will rely on these to do most of the work, because we only want to change one aspect of their behavior; most of this function is concerned with building a copy of the information associated with these descriptors. So we build a new list of descriptors, using our LocalizablePropertyDescriptor class (shown later). This is the class that will provide the localized name in its DisplayName property. Next, the EnsureAttrsRead method checks for the LocalizationResourceType attribute:
private void EnsureAttrsRead(object o) { if (resBase == null) { object[] attr = o.GetType().GetCustomAttributes( typeof(LocalizationResourceTypeAttribute), true); if (attr != null && attr.Length != 0) { resBase = ((LocalizationResourceTypeAttribute)

attr[0]).ResBase; } if (resBase == null) resBase = o.GetType(); }

This method is passed a single parameter: the object whose property names we are translating. It checks to see if that object's type definition has the LocalizationResourceType attribute. If it does, we store the Type object that it specifies in the resBase field. If the attribute is not present, we fall back to using the Type of the object itself. Next is the descriptor class itself. Rather than deriving directly from PropertyDescriptor, we use the helper base class provided by TypeConverter:
private class LocalizablePropertyDescriptor : TypeConverter.SimplePropertyDescriptor { private Type resBase; private string localizedName = null; private PropertyDescriptor realProp; public LocalizablePropertyDescriptor(Type resBase, PropertyDescriptor prop, Attribute[] attributes) : base (prop.ComponentType, prop.Name, prop.PropertyType, attributes) { this.resBase = resBase; realProp = prop; }

As before, the resBase property holds the Type object that will be used to initialize the ResourceManager. The localizedName field will hold the localized name once it has been looked upwe cache it here to avoid doing the lookup more than once. The realProp class holds a reference to the original PropertyDescriptor returned by TypeDescriptor.GetPropertieswe rely on this because our class does nothing more than localizing the display name. It defers to the real descriptor for everything else. The TypeConverter.SimplePropertyDescriptor class provides implementations for most of the abstract methods of PropertyDescriptor, but not GetValue or SetValue. This is because it doesn't presume that your descriptor will necessarily represent a real property, so it lets you implement them however you like. We just defer to the original PropertyDescriptor, which will just read and write the property respectively:
public override object GetValue(object component) { return realProp.GetValue(component); } public override void SetValue(object component, object value) {

realProp.SetValue(component, value);

Finally, we come to the part that this has all been building up tothe DisplayName property where we substitute the localized version of the name:
public override string DisplayName { get { if (localizedName == null) { ResourceManager rm = new ResourceManager(resBase); string tx = rm.GetString(base.DisplayName); if (tx != null) localizedName = tx; else localizedName = base.DisplayName; } return localizedName; } }

This looks almost identical to the core of the localizable category and description attributes. This is because they do much the same thingwe obtain a resource manager and use it to look up the localized string. This descriptor makes sure that it only does this lookup once, by caching the result in the localizedName property. So with this code in place, and the relevant attributes in use, all that is required are some suitable entries in the culture-specific resource file as shown in Figure 8-9. Our PropertyGrid is now fully localized, as Figure 8-10 shows. (The property values still look remarkably un-French, but because values are usually supplied by the user, it is not our job to localize them.) Figure 8-9. Localized strings for property names

Figure 8-10. The fully-localized PropertyGrid

There is a useful side effect of using these localization classes. Notice how in Figure 8-10 the DateOfBirth property has been translated with spaces between the words. Without our custom type converter in place, the displayed property names were just the real names as used in the source code, which precludes the use of spaces and most punctuation. But now we are free to use any text we like as the display name. You can employ such readability enhancements in your native languagethe ResourceManager is quite happy to look up resources even in the default culture, so long as you provide an appropriate .resx file. So if you supply a culture-neutral resource file, you can create entries mapping "DateOfBirth" onto "Date of Birth." So the fact that these classes allow you to decouple the display name from the real name is useful even when not translating text to another language. A culture-neutral resource file is one without a culture in the file name, such as CustomerDetails.resx. Such resources are built into the main assembly, not satellite assemblies.

There is one limitation with these classes. If you use the LocalizableTypeConverter, you can no longer use other converters, such as the ExpandableObject or converters of your own devising. For the latter this is fairly easy to fixsimply modify your own converters to inherit from LocalizableTypeConverter instead of TypeConverter. Of course, you can't do this with ExpandableObjectonly Microsoft gets to decide what that derives from. Fortunately, LocalizableTypeConverter already does everything that ExpandableObject does, so in practice it doesn't matter. Our CustomerDetails class has evolved since the version shown in Example 8-1, so Example 8-22 shows the modified class with all the relevant attributes in place. Example 8-22. CustomerDetails class with localizable categories and descriptions
[TypeConverter(typeof(LocalizableTypeConverter))] public class CustomerDetails { private CustomerName name; private string address; private DateTime dob;

[LocalizableCategory("Name", typeof (CustomerDetails))] [LocalizableDescription("Name.Description", typeof(CustomerDetails))] public CustomerName Name { get { return name; } set { name = value; } } [LocalizedCategory("Demographics", typeof (CustomerDetails))] [LocalizedDescription("DateOfBirth.Description", typeof(CustomerDetails))] public DateTime DateOfBirth { get { return dob; } set { dob = value; } } [LocalizedCategory("Location", typeof (CustomerDetails))] [LocalizedDescription("Address.Description", typeof(CustomerDetails))] public string Address { get { return address; } set { address = value; } } }

So, we have seen how to control which properties appear in the grid, what they are called, and how type conversions occur when moving data to and from the grid. But so far, the user interface for each individual property has consisted of nothing more exciting than an editable text field. We will now see how to add our own editing user interfaces to items on a PropertyGrid.

8.3 Custom Type Editors


The PropertyGrid allows us to replace the built-in text-based editing. We can assign a custom editor that supplies its own user interface. The framework calls such editors UI Type Editors. Not only can these provide a special-purpose editing user interface, they can change how the property's value is displayed even when we are not editing its value. Supplying a UI Type Editor is simple. We simply write a class that derives from System.Drawing.Design.UITypeEditor and associate it with the property or type in question using the Editor attribute. We only need to override two methods in our editor class. The first, GetEditStyle, is called to determine what style of editing UI we support; we can open a standalone modal dialog, drop down a UI in the PropertyGrid itself, or supply no editing UI. The second method, EditValue, is called when we are required to show our editing interface.

Let us add a new property to our CustomerDetails class (as shown in Example 8-22) so that we can supply a custom editing user interface for it. The new property is Happiness, and it indicates the level of customer satisfaction, on a range of 0 to 100%. It is shown in Examples Example 8-23 and Example 8-24. The editor has been specified with the Editor attribute. (The second parameter is always required to be UITypeEditor in the current version of the framework.) The property's type here is int or Integer, but we can provide custom UI editors for any type, whether it is a custom type or a built-in type. Example 8-23. Happiness property with editor using C#
private int happy;
[Editor(typeof(HappinessEditor), typeof(UITypeEditor))]

public int Happiness { get { return happy; } set { happy = value; } }

Example 8-24. Happiness property with editor using VB


Private happy As Integer <Editor(GetType( HappinessEditor), GetType(UITypeEditor))> _ Public Property Happiness As Integer Get Return happy End Get Set happy = Value End Set End Property

The simplest editing user interface that we can show is a modal dialog. Creating an editor class to provide this is very straightforward. Examples Example 8-25 and Example 8-26 show a custom type editor that simply presents a message box as its user interface. It asks the user if they are happy (yes or no), and sets their happiness as either 100% or 0% accordingly. (If the user hits cancel, the happiness is left unaltered.) Example 8-25. Modal dialog custom type editor using C#
public class HappinessEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { DialogResult rc = MessageBox.Show("Are you happy?", "Happiness", MessageBoxButtons.YesNoCancel); if (rc == DialogResult.Yes)

return 100; if (rc == DialogResult.No) return 0; return value; }

Example 8-26. Modal dialog custom type editor using VB


Public Class HappinessEditor Inherits UITypeEditor Public Overloads Overrides Function GetEditStyle( _ context As ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.Modal End Function Public Overloads Overrides Function EditValue( _ context As ITypeDescriptorContext, _ provider As IServiceProvider, _ value As Object) As Object Dim rc As MsgBoxResult = MsgBox("Are you happy?", _ MsgBoxStyle.YesNoCancel, "Happiness") If rc = MsgBoxResult.Yes Then Return 100 ElseIf rc = MsgBoxResult.No Then Return 0 End If Return value End Function End Class

The PropertyGrid will indicate the availability of this editor by placing a small button with an ellipsis in the property's value field when it is given the focus, as shown in Figure 8-11. The PropertyGrid knows to show the button because of the value returned by our editor class's GetEditStyle method. Figure 8-11. A modal editing UI offered in a PropertyGrid Modal dialog editors are easy to write, but they are usually not the most convenient kind of editor to use. Most of the system-supplied editors use the drop-down style, because it is less disruptive to the use of the program and makes it feel as if the property is an integrated part of the PropertyGrid. Showing a drop-down editor is almost as easy as showing a modal dialog. The main difference is that we have to supply a control rather than a form. A UserControl is likely to be the easiest option, although you can use a custom control. (In fact, you could even use one of the built-in controls.)

Examples Example 8-27 and Example 8-28 show a type editor that displays a control called HappinessControl. The most interesting part of this is the way in which it displays the control as a drop-down editor in the PropertyGrid. To do this, we must ask the grid for a service object. We do this through the IServiceProvider passed as the provider argumentthis is a generic interface that allows hosted components to ask their environment for certain facilities. In this case, we are asking the grid control for the service that lets us display drop-down editors, which is provided through the IWindowsFormsEditorService interface. Example 8-27. A drop-down type editor using C#
public class HappinessEditor : UITypeEditor { public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
IWindowsFormsEditorService wfes = provider.GetService( typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;

if (wfes != null) { HappinessControl hc = new HappinessControl(); hc.Value = (int) value;


wfes.DropDownControl(hc);

value = hc.Value; } return value; }

Example 8-28. A drop-down type editor using VB


Public Class HappinessEditor Inherits UITypeEditor Public Overloads Overrides Function GetEditStyle( _ context As ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.DropDown End Function Public Overloads Overrides Function EditValue( _ context As ITypeDescriptorContext, _ provider As IServiceProvider, _ value As Object) As Object Dim wfes As IWindowsFormsEditorService = _ CType(provider.GetService( _ GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService)

If Not wfes Is Nothing Then Dim hc As New HappinessControl() hc.Value = CInt(value) wfes.DropDownControl(hc) value = hc.Value End If Return value End Function End Class

When we call the DropDownControl method on the service object, it displays our control in the appropriate position on the PropertyGrid. It adjusts its size so that it is the same width as the value field, as shown in Figure 8-12. (The HappinessControl is just a UserControl. The most interesting code is the part that draws the face, which we will see shortly.) Figure 8-12. A drop-down editor in action

As you can see, in contrast to the rather dry yes/no interface provided by the MessageBox class or the VB MsgBox function, here we have gone for a slightly more emotive interface the customer satisfaction level is indicated by how happy or sad the face looks. The code from the Happiness control that draws this is shown in Examples Example 8-29 and Example 8-30. Example 8-29. Putting a happy face on the control using C#
public static void PaintFace(Graphics g, Rectangle r, int happiness) { r.Width -= 1; r.Height -= 1; float w = r.Width; float h = r.Height; // Draw face g.FillEllipse(Brushes.Yellow, r); g.DrawEllipse(Pens.Black, r); // Draw eyes float eyeLevel = h / 4; float eyeOffset = w / 4; float eyeSize = w / 6; g.FillEllipse(Brushes.Black, w/2 - eyeOffset, eyeLevel, eyeSize, eyeSize); g.FillEllipse(Brushes.Black, w/2 + eyeOffset - eyeSize + 1, eyeLevel, eyeSize, eyeSize);

// Draw smile float smileWidth = w/3; float smileLevel = (h*7)/10; float offs = ((happiness - 50) * h/4)/100; PointF[] points = { new PointF ((w - smileWidth)/2, smileLevel), new PointF ((w - smileWidth)/2, smileLevel + offs), new PointF ((w + smileWidth)/2+1, smileLevel + offs), new PointF ((w + smileWidth)/2+1, smileLevel) }; g.DrawBeziers(Pens.Black, points);

Example 8-30. Putting a happy face on the control using VB


Public Shared Sub PaintFace(g As Graphics, r As Rectangle, _ happiness As Integer) r.Width -= 1 r.Height -= 1 Dim w As Single = r.Width Dim h As Single = r.Height ' Draw face g.FillEllipse(Brushes.Yellow, r) g.DrawEllipse(Pens.Black, r) ' Draw eyes Dim eyeLevel As Single = h / 4 Dim eyeOffset As Single = w / 4 Dim eyeSize As Single = w / 6 g.FillEllipse(Brushes.Black, w/2 - eyeOffset, eyeLevel, _ eyeSize, eyeSize) g.FillEllipse(Brushes.Black, w/2 + eyeOffset - eyeSize + 1, _ eyeLevel, eyeSize, eyeSize) ' Draw smile Dim smileWidth As Single = w/3 Dim smileLevel As Single = (h*7)/10 Dim offs As Single = ((happiness - 50) * h/4)/100 Dim points() As PointF = _ { new PointF ((w - smileWidth)/2, smileLevel), _ new PointF ((w - smileWidth)/2, smileLevel + offs), _ new PointF ((w + smileWidth)/2+1, smileLevel + offs), _ new PointF ((w + smileWidth)/2+1, smileLevel) } g.DrawBeziers(Pens.Black, points) End Sub

The PropertyGrid control also lets us draw into the value field even when our editor is not running. Because the PaintFace method shown in Examples Example 8-29 and Example 8-30 has been written to scale its drawing to whatever space is available, we can call the same code from our HappinessEditor class to draw a small version of the face into the PropertyGrid, as shown in Examples Example 8-31 and Example 8-32.

Example 8-31. Custom value painting using C#


public override bool GetPaintValueSupported( ITypeDescriptorContext context) { return true; } public override void PaintValue(PaintValueEventArgs e) { System.Drawing.Drawing2D.SmoothingMode sm = e.Graphics.SmoothingMode; e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; HappinessControl.PaintFace(e.Graphics, e.Bounds, (int) e.Value); e.Graphics.SmoothingMode = sm; }

Example 8-32. Custom value painting using VB


Public Overloads Overrides Function GetPaintValueSupported( _ context As ITypeDescriptorContext) As Boolean Return True End Function Public Overloads Overrides Sub PaintValue(e As PaintValueEventArgs) Dim sm As SmoothingMode = e.Graphics.SmoothingMode e.Graphics.SmoothingMode = SmoothingMode.HighQuality HappinessControl.PaintFace(e.Graphics, e.Bounds, CInt(e.Value)) e.Graphics.SmoothingMode = sm End Sub

To get our drawing into the value field, we are required to override the GetPaintValueSupported method and return true. Having done this, the PropertyGrid will call our PaintValue method whenever it repaints the value field. We just call the static PaintFace method supplied by the HappinessControl. Notice how we turn on the high-quality smoothing mode on the Graphics object firstthis enables antialiasing, which is particularly important on small drawings. Without switching this on, the drawing would look rather ragged. Having changed the smoothing mode, it is important to restore it to its original value before the method terminates, because we are drawing with the same Graphics object that the PropertyGrid uses to draw itself. The results of this painting can be seen in Figure 8-13. Figure 8-13. Drawing in the value field

8.4 Summary
The PropertyGrid provides a simple but effective way of presenting data for editing. It uses reflection to discover which properties are available for editing. It allows every aspect of the property editing process to be controlled by using a TypeConverter. We can

manage conversions between the text displayed and edited onscreen, and the types stored in the object. We can control which properties are displayed, and how they are presented. We can even supply our own editor user interfaces for when simple text editing is insufficient. The PropertyGrid is also central to the Windows Forms Designer, so an understanding of how it works is crucial to integrating your controls with the development environment successfully. In the next chapter, we will look in detail at Visual Studio .NET's other design-time integration features.

You might also like