Creating a custom TypeConverter part 2 - Instance descriptors, expandable properties and standard values
In the first part of this article series, I described how to create a simple type converter for converting an object to and from a string. This follow up article expands upon that sample, to include more concise design time code generation, expandable property support and finally custom lists of values.
The examples in this article assume you are working from the original sample project from part 1.
Designer Code
When you place a Control
or Component
onto a design time
surface such as a Form
, the IDE will automatically generate
any code required to initialize the object.
Modify the SampleClass
class to inherit from Component
then
drop an instance onto the form and set the first property. Save
the form, then open the designer file. You should see code
something like this:
private void InitializeComponent()
{
CustomTypeConverter2.Length length1 = new CustomTypeConverter2.Length();
// ... SNIP ...
//
// sample
//
length1.Unit = CustomTypeConverter2.Unit.px;
length1.Value = 32F;
this.sample.Length1 = length1;
this.sample.Length2 = null;
this.sample.Length3 = null;
// ... SNIP ...
}
The designer has generated the source code required to populate the object by specifying each property individually. However, what happens if you wanted to set both properties at once or perhaps perform some other initialization code? We can use our type converter to solve this one.
Although slightly outside the bounds of this article, it's probably worth mentioning nonetheless. In the snippet above, you can see the
Length2
andLength3
properties are explicitly assignednull
, even though that is already the default value of these properties. If you're creating public facing library components it's always a good idea to apply theDefaultValue
attribute to properties. It makes for cleaner code (if the value is the default value, no code will be generated) and allows other components to perform custom processing if required. For example, thePropertyGrid
shows default properties in normal style, and non-default ones in bold.
Updating the Length class
Before we can adjust our type converter to support code
generation, we need to extend our Length
class by adding a new
constructor.
public Length()
{ }
public Length(float value, Unit unit)
: this()
{
this.Value = value;
this.Unit = unit;
}
I've added one constructor which will set both Value
and
Unit
properties of the class. Due to the addition of a
constructor with parameters, I now need to explicitly define a
parameterless constructor as an implicit one will no longer be
generated and I still want to be able to do new Length()
.
With these modifications in place, we can now dive into the type converter modifications.
CanConvertTo
The first thing we need to do is update our type converter to
state that it supports the InstanceDescriptor
class which is
the mechanism the IDE will use for the custom code generation.
We can do this by overriding a new method, CanConvertTo
.
Update the LengthConverter
class from the previous article to
include the following:
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
}
This new overloads will inform the caller that we now support
the InstanceDescriptor
type, in addition to whatever the base
TypeConverter
can handle.
Extending ConvertTo
We briefly covered the ConvertTo
override in the previous
article in order to display our Length
object as a string. Now
that we have overridden CanConvertTo
to state that we can
handle additional types, we need to update this method as well.
The InstanceDescriptor
class contains information needed to
regenerate an object, and is comprised of two primary pieces of
information.
- A
MemberInfo
object which describes a method in the class. This can either be a constructor (which we'll use in our example), or something static that will return a new object - for example,Color.FromArgb
. - An
ICollection
containing any of the arguments required to pass into the source member.
Lets update ConvertTo
to include the extract support.
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
Length length;
object result;
result = null;
length = value as Length;
if (length != null)
{
if (destinationType == typeof(string))
result = length.ToString();
else if (destinationType == typeof(InstanceDescriptor))
{
ConstructorInfo constructorInfo;
constructorInfo = typeof(Length).GetConstructor(new[] { typeof(float), typeof(Unit) });
result = new InstanceDescriptor(constructorInfo, new object[] { length.Value, length.Unit });
}
}
return result ?? base.ConvertTo(context, culture, value, destinationType);
}
We still do our null check to ensure we have a valid value to
convert, but now we check to see if the type is either string
or InstanceDescriptor
and process accordingly.
For instance descriptors, we use Reflection in order to get the
constructor which takes two parameters, and then we create an
InstanceDescriptor
object from that. Easy enough!
Now when we modify our SampleClass
component in the designer,
source code is generated similar to the below. (With the caveat
of the warning in the next section)
Note that I'd also modified the properties on the SampleClass
to include [DefaultValue(typeof(Length), "")]
for default
value support.
private void InitializeComponent()
{
// ... SNIP ...
//
// sample
//
this.sample.Length1 = new CustomTypeConverter2.Length(16F, CustomTypeConverter2.Unit.px);
// ... SNIP ...
}
Much cleaner!
A warning on Visual Studio
While writing this article, Visual Studio frequently took a huff
and refused to generate the design time code. I assume it is due
to Visual Studio caching the assembly containing the
TypeConverter
, or it is another manifestation of not being
able to unload managed assemblies without destroying the
application domain. Whatever the reason, I found it quickly to
be a source of frustration requiring frequent restarts of the
IDE in order to pick up changed code.
As an experiment, I did a test where the Length
and
LengthConverter
classes were in another assembly referenced in
binary form. In this mode, I didn't have a single problem.
Finally, whereas basic conversions are easy to debug, the
InstanceDescriptor
conversion is much less so.
Something to bear in mind.
Expandable properties
Returning to the ExpandableObjectConverter
and property
expansion, that is trivially easy to add to your custom
converter by overriding the GetPropertiesSupported
and
GetProperties
methods.
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
return TypeDescriptor.GetProperties(value, attributes);
}
First, by overriding GetPropertiesSupported
we tell the caller
that we support individual property editing. Then we can
override GetProperties
to return the actual properties to
display.
In the above example, we return all available properties, which
is probably normal behaviour. Let us assume the Length
class
has a property on it which we didn't want to see. We could
return a different collection with that property filtered out:
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
//return TypeDescriptor.GetProperties(value, attributes);
return new PropertyDescriptorCollection(TypeDescriptor.GetProperties(value, attributes).Cast<PropertyDescriptor>().Where(p => p.Name != "BadProperty").ToArray());
}
An awkward example, but it does demonstrate the feature.
The property grid honours the
Browsable
attribute - this is a much better way of controlling visibility of properties than the above!
Custom Values
The final example I want to demonstrate is custom values.
Although you might assume that you'd have to create a custom
UITypeEditor
, if you just want a basic drop down list, you can
do this directly from your type converter by overriding
GetStandardValuesSupported
and GetStandardValues
.
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
List<Length> values;
values = new List<Length>();
values.Add(new Length(16, Unit.px));
values.Add(new Length(32, Unit.px));
values.Add(new Length(64, Unit.px));
values.Add(new Length(128, Unit.px));
return new StandardValuesCollection(values);
}
First you need to override GetStandardValuesSupported
in order
to specify that we do support such values. Then in
GetStandardValues
we simply return the objects we want to see.
In this example, I've generate 4 lengths which I return. When
you run the program, you can see and select these values. Of
course, you need to make sure that the values you return can be
handled by the ConvertFrom
method!
Summing up
Adding even an advanced type converter is still a easy task, and is something that can help enrich editing functionality.
You can download the complete example from the link below.
Update History
- 2013-07-28 - First published
- 2020-11-21 - Updated formatting
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
CustomTypeConverter2.zip
|
Example project showing how to create a simple custom type converter that covers more advance functionality, such as |
1.0.0.0 | 28/07/2013 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Charlie Cole
#
Thanks
Oscar Zatarain
#
Thanks for the tutorial I'm a little (much) confused due to I'm new doing this things, can you help me with this scenario?:
I need to create a component ControlValidator : It provides a property called 'Validator', 'Validator' type is ValidatorBase
I want to display the property 'Validator' in the designer as a Combo that displays 'None, Required Field, Regular Expression',
Where: None = No Validations Required Field = Class that extends from ValidatorBase Regular Expression = Class that extends from ValidatorBase
Is there a way to achieve this?
Reza Shamayel
#
Hi Rechard,
Very thanks of you for this tutorial. I study the part1 and part2 completely. In the part2, you told that has a problem with IDE (under "A warning on Visual Studio" section). I have same problem in my solution, exactly. Whenever I save my solution and rebuild it, then change the property value, any try to save form, give me an error in IDE. Error message tell me that my converter can not convert from my class to InstanceDescriptor. Same error occur in your project exactly.
What is the solution for this error message?
Best regards Reza
Richard Moss
#
Reza,
The only time this has happened to me, and the solution I found, was not to have the type converter source in the same project as whatever code was actually using it, and to use that as a binary reference, not just shift it to another project referenced in source form. Sorry I can't really help, what I posted above is what I experienced when I was experimenting with type converters. I haven't really used them since, as I tend to either use pre-built converters or designers.
Regards;
Richard Moss
Memo
#
To fix under "A warning on Visual Studio" section
https://stackoverflow.com/questions/43019832/auto-versioning-in-visual-studio-2017-net-core or use PrecisionInfinityAutomaticVersions3 tool for that. or https://stackoverflow.com/questions/71191341/how-to-change-assemblyinfo-cs-assemblyversion-with-date-time-and-increment-revis
Mohamed
#
Your website are awesome these blogs helped us