Chapter 20. Using the Property Grid Control with Late Binding

 

The only way to discover the limits of the possible is to go beyond them into the impossible.

 
 --Arthur C. Clarke

With the advent of the Microsoft .NET platform, development time has been decreased significantly because of many improvements to workflow and the technologies we use. Perhaps one of the most exciting introductions is the idea behind extensible metadata and reflection, which can be used to interrogate class properties, methods, and attributes.

Many tools and utilities have a need to work with class objects and also provide a way to modify the properties of a class. Traditionally, a dialog would have been built that contained controls which, when modified, would find the currently selected class and alter the appropriate property; building this dialog is often a very time-consuming and tedious task. If you work with Visual Studio .NET, you will have interacted with the PropertyGrid control, which displays information about a selected user interface element. Figure 20.1 shows the PropertyGrid control in action within Visual Studio .NET.

Screenshot of a PropertyGrid in the Visual Studio .NET IDE.

Figure 20.1. Screenshot of a PropertyGrid in the Visual Studio .NET IDE.

A PropertyGrid can be bound to any managed object and programmatically build a user interface that can modify the public properties of the object with hardly any work! In this chapter, I will show you how to create a class with bindable properties, and then show you how to bind an instance of this class to a PropertyGrid control. There is too much information on the PropertyGrid to be covered in a short chapter, but the core functionality should be summarized enough to be applicable to the majority of tools and utilities.

Designing a Bindable Class

There really is no configuration that has to happen on the PropertyGrid control, because all the configuration information is specified in the classes that are bound to the PropertyGrid. The PropertyGrid control interrogates bound classes to find certain attributes of properties that describe things, such as what category they are in, the description of the property, and what the default value is. You can also hide properties from being shown in the PropertyGrid with an attribute as well.

The [DefaultPropertyAttribute] specifies the name of the property that will act as the default property for the PropertyGrid.[CategoryAttribute] specifies the name of the category that the property is located in. Categories are automatically created based on these names. The [DescriptionAttribute] specifies the description text that appears at the bottom of the PropertyGrid when a property is selected. The TypeConverter and PropertyOrder attributes will be covered in the next section. You can create read-only properties simply by providing a get construct. You can also hide properties from showing up in the PropertyGrid by using a [Browsable(false)] attribute.

The following code shows an example of a bindable class that can be visualized and modified using the PropertyGrid control. Notice the attributes that are used to specify names, descriptions, and ordering for the visualized properties.

public enum Gender
{
    Male,
    Female,
    Unspecified
}

public enum Position
{
    Programmer,
    Tester,
    Director,
    Architect,
    Analyst,
    Unspecified
}

[TypeConverter(typeof(PropertyOrderConverter)),
DefaultPropertyAttribute("FirstName")]
public class PersonnelRecord
{
    // Contact Information
    private string firstName;
    private string lastName;
    private string phoneNumber;
    private string email;

    // Biological Information
    private DateTime birthDate;
    private int age;
    private Color hairColor;
    private Color eyeColor;
    private Gender gender;

    // Employee Information
    private int employeeId;
    private Position position;
    private bool probationary;

    public PersonnelRecord()
    {
        firstName = String.Empty;
        lastName = String.Empty;
        phoneNumber = String.Empty;
        email = String.Empty;

        birthDate = new DateTime();
        age = 0;
        gender = Gender.Unspecified;

        employeeId = 0;
        position = Position.Unspecified;
        probationary = true;
    }

    [CategoryAttribute("Contact Information"),
    DescriptionAttribute("First name of the employee."),
    PropertyOrder(0)]
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }

    [CategoryAttribute("Contact Information"),
    DescriptionAttribute("Last name of the employee."),
    PropertyOrder(1)]
    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }

    [CategoryAttribute("Contact Information"),
    DescriptionAttribute("Phone number of the employee. (###-###-####)"),
    PropertyOrder(2)]
    public string PhoneNumber
    {
        get { return phoneNumber; }
        set { phoneNumber = value; }
    }

    [CategoryAttribute("Contact Information"),
    DescriptionAttribute("Email of the employee. Format: *@*.*"),
    PropertyOrder(3)]
    public string Email
    {
        get { return email; }
        set { email = value; }
    }

    [CategoryAttribute("Biological Information"),
    DescriptionAttribute("Birth date of the employee."),
    PropertyOrder(0)]
    public DateTime BirthDate
    {
        get { return birthDate; }
        set
        {
            birthDate = value;
            age = DateTime.Now.Year - birthDate.Year;
        }
    }

    [CategoryAttribute("Biological Information"),
    DescriptionAttribute("Age of the employee."),
    PropertyOrder(1)]
    public int Age
    {
         get { return age; }
    }

    [CategoryAttribute("Biological Information"),
    DescriptionAttribute("Hair color of the employee. (Optional)"),

    PropertyOrder(2)]
    public System.Drawing.Color HairColor
    {
        get { return hairColor; }
        set { hairColor = value; }
    }

    [CategoryAttribute("Biological Information"),
    DescriptionAttribute("Eye color of the employee. (Optional)"),
    PropertyOrder(3)]
    public System.Drawing.Color EyeColor
    {
        get { return eyeColor; }
        set { eyeColor = value; }
    }

    [CategoryAttribute("Biological Information"),
    DescriptionAttribute("Gender of the employee. (Optional)"),
    PropertyOrder(4)]
    public Gender Gender
    {
        get { return gender; }
        set { gender = value; }
    }

    [CategoryAttribute("Employee Information"),
    DescriptionAttribute("Id of the employee as referenced by the HR database."),
    PropertyOrder(0)]
    public int EmployeeId
    {
        get { return employeeId; }
        set { employeeId = value; }
    }

    [CategoryAttribute("Employee Information"),
    DescriptionAttribute("Position of the employee within the organization."),
    PropertyOrder(1)]
    public Position Position
    {
        get { return position; }
        set { position = value; }
    }

    [CategoryAttribute("Employee Information"),
    DescriptionAttribute("True or false value indicating a probationary period."),
    PropertyOrder(2)]
    public bool Probationary
    {
        get { return probationary; }
        set { probationary = value; }
    }
}

Ordering Properties

Strangely enough, there is no attribute that handles the ordering of properties in the PropertyGrid. There is, however, a way we can make our own attribute and custom type converter that can accomplish this for us. First, we will define an attribute that we can use to specify the sort order for properties in a class.

[AttributeUsage(AttributeTargets.Property)]
public class PropertyOrderAttribute : Attribute
{
    private int order;

    public PropertyOrderAttribute(int order)
    {
        this.order = order;
    }

    public int Order
    {
        get { return order; }
    }
}

The following code describes a custom type converter class that interrogates the PropertyOrder attribute in class properties, sorts the property list based on the values, and returns a descriptor list that can tell the PropertyGrid the order to display the properties in.

public class PropertyOrderConverter : ExpandableObjectConverter
{
    internal class SortablePair : IComparable<SortablePair>
    {
        private int order;
        private string name;

        public string Name
        {
            get { return name; }
        }

        public SortablePair(string name, int order)
        {
            this.order = order;
            this.name = name;
        }

        public int CompareTo(SortablePair pair)
        {
            int result;

            if (pair.order == order)
            {
                result = string.Compare(name, pair.name);
            }
            else if (pair.order > order)
            {
                result = -1;
            }
            else
            {
                result = 1;
            }

            return result;
     }
}

public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
    return true;
}

public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext cx,
                                                                  object component,
                                                                Attribute[] attrib)

       {
       List<SortablePair> propertyList = new List<SortablePair>();

       PropertyDescriptorCollection descList = TypeDescriptor.GetProperties(component,
                                                                            attrib);

       foreach (PropertyDescriptor descriptor in descList)
       {
               Attribute attribute
               = descriptor.Attributes[typeof(PropertyOrderAttribute)];

       if (attribute != null)
       {
       PropertyOrderAttribute orderAttribute = (PropertyOrderAttribute)attribute;
                   propertyList.Add(new SortablePair(descriptor.Name,
                                                     orderAttribute.Order));
               }
               else
               {
                   propertyList.Add(new SortablePair(descriptor.Name, 0));
               }
           }

           propertyList.Sort();

           List<String> propertyNames = new List<String>();

           foreach (SortablePair sortablePair in propertyList)
           {
               propertyNames.Add(sortablePair.Name);
           }

           return descriptorList.Sort(propertyNames.ToArray());
     }
}

Using the type converter class is fairly easy. Just decorate your class declaration with the attribute, as shown in the following code. Then decorate your properties with a PropertyOrder attribute to specify the sort order.

[TypeConverter(typeof(PropertyOrderConverter))]
public class PersonnelRecord
{
    // ...
}

Note

I did have a workaround for category ordering in Microsoft .NET 1.1, but this workaround had undesired results when used with .NET 2.0. At this point in time, I have not figured out a way to do this.

Using the PropertyGrid

With a bindable class created and a custom TypeConverter created to handle property ordering, using the PropertyGrid control is super easy. All you need to do is drag the PropertyGrid control from the Visual Studio .NET toolbox onto your form. The only other thing you need to do now is set the SelectedObject property of the PropertyGrid, to an instance of our bindable class PersonnelRecord.

Figure 20.2 shows the PropertyGrid control item in the Visual Studio .NET toolbox.

Screenshot of the PropertyGrid control item in the Visual Studio .NET toolbox.

Figure 20.2. Screenshot of the PropertyGrid control item in the Visual Studio .NET toolbox.

The following code snippet shows the load event for the main form in the accompanying example. Notice how easy it is to instantiate our PersonnelRecord, set some initial values, and then bind it to the PropertyGrid.

private void MainForm_Load(object sender, EventArgs e)
{
    PersonnelRecord record = new PersonnelRecord();

    record.FirstName    = "John";
    record.LastName     = "Smith";
    record.PhoneNumber  = "555-123-4567";
    record.Email        = "[email protected]";

    record.BirthDate    = Convert.ToDateTime("1980-04-10");
    record.HairColor    = Color.Brown;
    record.EyeColor     = Color.Blue;
    record.Gender       = Gender.Male;
    record.EmployeeId   = 12345;
    record.Position     = Position.Programmer;
    record.Probationary = false;

    PropertyGridEditor.SelectedObject = record;
}

Running the code snippet that instantiates a PersonnelRecord with initial values and binds it to the PropertyGrid will produce results similar to those shown in Figure 20.3.

Screenshot of the accompanying PropertyGrid example.

Figure 20.3. Screenshot of the accompanying PropertyGrid example.

The Companion Web site contains the full source code to the bindable class, PropertyOrder type converter, and example usage.

Conclusion

This chapter discussed the implementation details around the PropertyGrid control in the .NET framework. As mentioned before, the bulk of the implementation lies in attribute decoration in the bindable class, since all you need do to use the PropertyGrid is instantiate a PropertyGrid control and set the SelectedObject property to your class instance.

It should be noted that in this chapter I covered a large chunk of the implementation details, but there was no coverage of localization of properties or the development of custom type editors. Both subjects require a fair amount of explanation and code. Feel free to visit MSDN to investigate these features.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.255.36