Playing with Property Grids in C# – Part 2

In Part 1 we saw how easy it is to use a PropertyGrid control for a user interface with user-configurable options. Here we expand on that a little.

Customizing the appearance of an enum

I mentioned that we will see how to handle properties for types that themselves contain expandable properties, but before that, here is something else that can be done as a step in that direction – customize the way an enumeration is displayed.

Enum types, when used for properties, are expanded automatically, and normally that is exactly what you would want. In the example used in Part 1, I exposed the default setting  for the video converter’s process priority.

This is of type System.Diagnostics.ProcessPriorityClass, so it will populate a ComboBox (read-only i.e. a DropDownListBox) with Normal, Idle, High, RealTime, BelowNormal, and AboveNormal. That’s great for developers, who know what an enumerated type is, but for normal users, it would be better to split up the words like BelowNormal and display “Below Normal” instead.

This is what the options dialog looks like now, with the Process Priority property expanded:

OptionsWithStringConverter

To achieve this, I cheated a little (because that’s the easiest way to do this)…

In the type that’s shown in the grid, I now hide the real property that was expanded, and add a new property, which is just a string. This, as it happens, is an easy way to add three new tools to our property grid tweaking toolbox:

  • The System.ComponentModel.BrowsableAttribute, which is used to enable or disable showing a public property on the grid.
  • The concept of a TypeConverter, which can be used to extend the behaviour of an item that’s shown on the grid, by enabling the conversion between it and other types.
  • The System.ComponentModel.TypeConverterAttribute, which associates a TypeConverter with a member or type.

These are the relevant properties of my Preferences type:

  1.  
  2. [Browsable(false)]
  3. public ProcessPriorityClass VideoConverterPriority
  4. {
  5.     get { return Romy.VideoConvertor.Properties.Settings.Default.Priority; }
  6.     set { Romy.VideoConvertor.Properties.Settings.Default.Priority = value; }
  7. }
  8.  
  9. [TypeConverter(typeof(ProcessPriorityStringConverter)), Category(“Video Converter”), DisplayName(“Process Priority”), DefaultValue(“Below Normal”),
  10. Description(“Specifies the initial process priority for converting videos. You can change it for an individual conversion from the progress dialog.”)]
  11. public string VideoConverterPriorityName
  12. {
  13.     get { return videoConverterPriorityName; }
  14.     set
  15.     {
  16.         videoConverterPriorityName = value;
  17.         VideoConverterPriority = ProcessPriorityStringConverter.PriorityClasses.
  18.             ToArray()[Array.IndexOf(ProcessPriorityStringConverter.FormatNames.ToArray(),
  19.             videoConverterPriorityName)];
  20.     }
  21. }

 

As you can see, the new VideoConverterPriorityName property uses a TypeConverterAttribute, and when set, it in turn sets the now hidden VideoConverterPriority property.

The TypeConverter used here is of the derived StringConverter type:

A StringConverter to define standard values of a string
  1. public class ProcessPriorityStringConverter : StringConverter
  2. {
  3.     private static readonly string[] formatNames = new string[] {
  4.         “Normal”, “Idle”, “High”, “Real Time”, “Below Normal”, “Above Normal” };
  5.  
  6.     private static readonly ProcessPriorityClass[] priorityClasses = new ProcessPriorityClass[] {
  7.         ProcessPriorityClass.Normal, ProcessPriorityClass.Idle, ProcessPriorityClass.High,
  8.         ProcessPriorityClass.RealTime, ProcessPriorityClass.BelowNormal, ProcessPriorityClass.AboveNormal };
  9.  
  10.     public static IEnumerable<ProcessPriorityClass> PriorityClasses
  11.     {
  12.         get { return ProcessPriorityStringConverter.priorityClasses; }
  13.     }
  14.  
  15.     public static IEnumerable<string> FormatNames
  16.     {
  17.         get { return ProcessPriorityStringConverter.formatNames; }
  18.     }
  19.  
  20.     public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
  21.     {
  22.         /* Since these values are just strings, return true here
  23.          * to prevent the user from being able to edit them. */
  24.         return true;
  25.     }
  26.  
  27.     public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
  28.     {
  29.         return true;
  30.     }
  31.  
  32.     public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
  33.     {
  34.         return new StandardValuesCollection(formatNames);
  35.     }
  36. }

 

What we want is to display a ComboBox for the new string property, which needs to be read-only. To achieve this, the ProcessPriorityStringConverter class needs to:

  • Override the virtual bool GetStandardValuesSupported(ITypeDescriptorContext context) and return true. This will cause the property to display in a ComboBox in the grid, but the items will be editable.
  • Override the virtual bool GetStandardValuesExclusive(ITypeDescriptorContext context) and return true. This will make the values read-only.
  • Override the virtual StandardValuesCollection GetStandardValues(ITypeDescriptorContext context), and return the values that must be displayed.

That was easy. Now let’s see something more interesting.

Handling expandable properties in the grid

My video converter wraps the ffmpeg console application for Windows (that I download frequently, since it is updated often, from this site) using a System.Diagnostic.Process component .

It supports a limited set of the video and audio filters that are supported by ffmpeg, which it just passes along to the process as command-line arguments.

One of the video filters it supports is crop, which is simply passed along in the format “width:height:X:Y”. Thus my Crop type is a struct with two properties:

  • Size, which is of type System.Drawing.Size
  • Location, which is a System.Drawing.Point

The Crop type’s ToString method formats the two properties and returns them in the required format, and that is what will be shown on the grid. But that is all that will happen without some more code. Of course, what it needs to do is expand its properties, and automatically update them when the parent item’s value is edited in the grid.

Below is what it looks like, where Crop is shown near the bottom of the grid on the left. (Here I pretended that the movie I downloaded needed to be cropped, and typed directly into the value displayed for Crop, that is the parent item, in the grid, which was initialized to the actual video size and a location at the top left. – in this case 720:304:0:0.)

CropExpandedjpg

To enable the behaviour we need, a different TypeConverter-derived type must be used, an ExpandableObjectConverter. Actually this is even easier than the StringConverter we’ve seen so far. It looks like this:

An ExpandableObjectConverter
  1. public class CropTypeConverter : ExpandableObjectConverter
  2. {
  3.     public override bool CanConvertFrom(ITypeDescriptorContext context,
  4.         Type sourceType)
  5.     {
  6.         return sourceType == typeof(string);
  7.     }
  8.  
  9.     public override bool CanConvertTo(ITypeDescriptorContext context,
  10.         Type destinationType)
  11.     {
  12.         return destinationType == typeof(Crop);
  13.     }
  14.  
  15.     public override object ConvertFrom(ITypeDescriptorContext context,
  16.         System.Globalization.CultureInfo culture, object value)
  17.     {
  18.         var s = (string)value;
  19.  
  20.         if (s != null)
  21.             return (Crop)s;
  22.  
  23.         return base.ConvertFrom(context, culture, value);
  24.     }
  25.  
  26.     public override object ConvertTo(ITypeDescriptorContext context,
  27.         System.Globalization.CultureInfo culture, object value,
  28.         Type destinationType)
  29.     {
  30.         if (destinationType == typeof(System.String) && value is Crop)
  31.             return ((Crop)value).ToString();
  32.  
  33.         return base.ConvertTo(context, culture, value, destinationType);
  34.     }
  35. }

 

The ExpandableObjectConverter-derived type needs to:

  • Override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) and return true if the sourceType passed in is a string.
  • Override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) and return true if the destinationType passed in is a Crop instance.
  • Override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value), convert the value passed in (which should be a string) to a Crop instance and return it.
  • Override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType), convert the value passed in (which should be a Crop) to a string, and return it.

Notice that in my ConvertFrom implementation, I type-cast a string to a Crop. That shouldn’t work… but instead throw an Exception, because the framework obviously does not know how to convert a string to a Crop. I added an explicit conversion operator to the Crop type. (I didn’t have to do it that way, but found it the most convenient, since nobody else should try to call the operator besides myself, and I only ever do so from the CropTypeConverter.) This is the code that was called when I typed the new Crop value into the grid.

Below is a slightly simplified representation of the Crop type. (I removed other operators and methods that are not relevant to this post.) Notice that this time the TypeConverterAttribute is used to associate the TypeConverter with the type itself, not a member like the previous example.

  1. [TypeConverter(typeof(CropTypeConverter))]
  2. public struct Crop
  3. {
  4.     private Point location;
  5.     private Size size;
  6.  
  7.     public Crop(int width, int height, int x, int y)
  8.     {
  9.         size = new Size(width, height);
  10.         location = new Point(x, y);
  11.     }
  12.  
  13.     public Crop(Point location, Size size)
  14.     {
  15.         this.location = location;
  16.         this.size = size;
  17.     }
  18.  
  19.     [Browsable(false)]
  20.     public static Crop Empty
  21.     {
  22.         get { return new Crop { Size = Size.Empty, Location = Point.Empty }; }
  23.     }
  24.  
  25.     [Category(“Data”),
  26.     Description(“The point at which to begin cropping the video.”)]
  27.     public Point Location
  28.     {
  29.         get { return location; }
  30.         set { location = value; }
  31.     }
  32.  
  33.     [Category(“Data”),
  34.     Description(“The size of the cropped video.”)]
  35.     public Size Size
  36.     {
  37.         get { return size; }
  38.         set { size = value; }
  39.     }
  40.  
  41.     public static explicit operator Crop(string value)
  42.     {
  43.         if (value.Contains(“:”))
  44.         {
  45.             var values = value.Split(‘:’);
  46.  
  47.             if (values.Length == 4)
  48.             {
  49.                 int width = 0, height = 0, x = 0, y = 0;
  50.  
  51.                 if (int.TryParse(values[0], out width) &&
  52.                     int.TryParse(values[1], out height) &&
  53.                     int.TryParse(values[2], out x) &&
  54.                     int.TryParse(values[3], out y))
  55.                 {
  56.                     return new Crop(width, height, x, y);
  57.                 }
  58.             }
  59.         }
  60.         return Crop.Empty;
  61.     }
  62.  
  63.     public override string ToString()
  64.     {
  65.         return string.Format(“{0}:{1}:{2}:{3}”, Size.Width, Size.Height, Location.X, Location.Y);
  66.     }
  67. }

 

And that’s it. Easy! As you may have noticed in the screenshot, there are other expandable properties besides those described. They use the same principle.

There are even more things you can do to customize the user interface around a property grid, such as creating your own property editors. I have never needed to do so myself, but if you find that you need to, have a look at this walkthrough on the MSDN Library site.

Update: There is also a wider usage of type converters than what is mentioned here. I just noticed that Scott Hanselman wrote an interesting article on type converters a while back in 2008. It is worth exploring for interest and having a better understanding of the framework in general.

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s