[ Team LiB ] |
8.1 Displaying Simple ObjectsThe 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 VBPublic 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.
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 PropertyGridThe 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 descriptionsAs 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 CategoriesThe 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 fileFigure 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.
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 namesWe 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 |
[ Team LiB ] |