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:
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:
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:
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.)
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:
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.
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.