DekGenius.com
[ Team LiB ] Previous Section Next Section

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
figs/winf_0801.gif

The grid is shown here with its default settings—the 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 off—setting 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
figs/winf_0802.gif

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 display—the 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 list—any 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 object—resources 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
figs/winf_0803.gif

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, Démographiques. 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
figs/winf_0804.gif

We are not done yet—the description and property names are still in English. The description can be fixed in much the same way that categories were—we 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 different—we 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);
      string tx = rm.GetString(DescriptionValue);
      if (tx != null)
          DescriptionValue = tx;
  }
  return base.Description;
        }
    }
}
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 category—it 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
figs/winf_0805.gif
    [ Team LiB ] Previous Section Next Section