DekGenius.com
[ Team LiB ] Previous Section Next Section

8.2 Type Conversion

The PropertyGrid is able to edit many different kinds of data, and can provide special-purpose 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 text—its 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
figs/winf_0806.gif

The field is displayed, but not with any useful information—just 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 slightly—as 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
figs/winf_0807.gif

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 CustomerName—values 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 these—the 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 names—it 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 properties—just 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 thing—PropertyGrid 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 expandable—a 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
figs/winf_0808.gif

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 CustomerName—the 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 achieve—simply 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 types—there 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 use—nested 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[1]—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 rescue—it 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.

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[2] 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] 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 it—it 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 directly—it 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 stands—you 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 problem—after 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 directly—it 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 TypeConverter—this 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 this—because 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 project—it 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 up—we cache it here to avoid doing the lookup more than once. The realProp class holds a reference to the original PropertyDescriptor returned by TypeDescriptor.GetProperties—we 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 to—the 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 thing—we 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
figs/winf_0809.gif
Figure 8-10. The fully-localized PropertyGrid
figs/winf_0810.gif

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 language—the 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 fix—simply modify your own converters to inherit from LocalizableTypeConverter instead of TypeConverter. Of course, you can't do this with ExpandableObject—only 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.

    [ Team LiB ] Previous Section Next Section