Introduction
Our experiences with Visual Studio 2005™ demonstrate that function calls are not fired in outer set
accessors of properties subject to TypeConverters
. This article first revisits how to deploy TypeConverters
so that your class properties will be displayed from a nested node in Properties
View. It then demonstrates the necessary pattern for declaring DefaultValueAttributes
, and for writing set
accessors that will successfully fire vital accessor functions.
Background
A rudimentary and very common chore of writing class libraries is defining properties comprised of custom classes or structs. As with Font
, Size
, Location
, or Anchors
, a developer will practically always want to adhere to conventions which display the class/struct property in an expandable property node at design time.
This is a job we should be able to get done in just a few minutes. Product documentation and nomenclature however make it difficult to find reference material on nested node behavior, and fail altogether to explain what constraints are imposed on DefaultValueAttributes
or what hurdles obstruct functions we expect to perform in property accessors. You can spend days on these regular design issues instead of minutes without the simple instructions which follow.
This example exposes a ColorOffset
property for a custom button class, producing the following nested node and reset behavior in the IDE.
"TypeConverters" Provide Nested Node Behavior
Thanks to very unhelpful documentation, many developer's nested node behavior efforts might remain dead in the water until they discover material such as a Code Project article, "Creating Custom Controls-Providing Design Time Support 1" by Kodanda Pani. Pani explains that we have to build a TypeConverter
for the class of our property, overriding GetPropertiesSupported( )
to return true; and overriding GetProperties( )
to pass the class of our property to the base implementation. The latter reflects our subproperties to the nested node. This is all there is to the necessary TypeConverter
:
C# Example - TypeConverter For a GCColorOffset Class
public class GCColorOffsetTypeConverter :
System.ComponentModel.TypeConverter
{
public override bool GetPropertiesSupported( ITypeDescriptorContext context )
{
return true;
}
public override PropertyDescriptorCollection GetProperties
( ITypeDescriptorContext context, object value, Attribute[ ] attributes )
{
return TypeDescriptor.GetProperties( typeof( GCColorOffset ) );
}
}
Browsable
and Serializable
are required on the property class definition; and our TypeConverter
is associated with this GCColorOffset
class by a TypeConverterAttribute
:
C# Example - TypeConverterAttribute Associates GCColorOffsetTypeConverter With GCColorOffset
[Browsable( true )]
[Serializable]
[TypeConverter( typeof( GCColorOffsetTypeConverter ) )]
public class GCColorOffset
{
Now, whenever we declare a GCColorOffset
property in an outer class, the IDE will provide our GCColorOffset
subproperties in a nested node of the outer class.
In the Outer Class, Declare the Property *Without a DefaultValueAttribute*
As Pani also shows, we must declare the DesignerSerializationVisibility
and Browsable
attributes on the property definitions we may now make in outer classes.
To our great displeasure however, and at incredible wastes of work, we found that acceptable IDE behavior was impossible if we declared DefaultValueAttributes
on property declarations in outer classes. DefaultValueAttributes
would compile, and they might even work once, but thereafter reset behavior would crash; property accessors would fail to read or write, and so forth.
Exhaustive experimentation found that DefaultValueAttributes
could only be declared directly on the bottom/inner-most property class members (GCColorOffset.BaseProperty
). Almost certainly however, good reasons will compel you to declare DefaultValueAttributes
on the outer class property, because it is the outer class to which the default is associated, and because usual conditions require a given DefaultValue
in one outer class while yet other outer classes require different DefaultValues
. As these are standard patterns which class designs must contend with, forcing us to declare DefaultValueAttributes
on the inner property class denies us the DefaultValue
flexibility that class library designs require.
Costs Of Discovering You Cannot Declare DefaultValueAttributes in The Outer Class
It is practically impossible therefore that class design efforts will never encounter this issue. Because composite classes are inevitable design paths, this flaw or weakness of the IDE will cost all class engineers dearly, as we first struggle for great whiles to make DefaultValueAttributes
work in outer classes, only to find after exhaustive wasted efforts that the IDE blows itself out of the water if we do so.
That this weakness is not documented — even by a recommended pattern for declaring DefaultValueAttributes
— only compounds our injuries, because we can only solve this obstructive behavior by conceding to a pattern which is adverse to inevitable purposes of class design. Needing to associate DefaultValues
with the outer class, every temptation to place them in inner classes will be strenuously resisted. Why? Because you will want to avoid an obvious further problem (and bad design) this imposes: Your further class design efforts will suffer the further cost of having to subclass inner property classes merely to declare different defaults.
Costs Of Discovering That Vital Functions Are Never Fired in Outer Class Accessors
In the very next moments of this inevitable process however, we discover a further amazing thing — that vital function calls in outer class property accessors are not fired by the IDE. Indeed, design time execution of set
accessors simply jumps across vital instructions as if they are meant to be ignored. Ignored calls are preserved below:
C# Example - Outer Class Property Definition
[Browsable( true )]
[Category( "Appearance" )]
[Description( "Tints and/or adjusts the luminosity of system colors.
Derivatives survive distribution and OnSystemColorsChanged.
Default = 0, 0, 0." )]
[DesignerSerializationVisibility( DesignerSerializationVisibility.Content )]
public GCColorOffset ColorOffset
{
get
{
return f_ColorOffset;
}
set
{
f_ColorOffset = value;
}
}
In other words, at design time, outer class accessors of properties subject to TypeConverters
can perform no validation, no function calls... you get the picture. Thus, you will have to perform vital accessor functions too from the same improbable and unreasonable place — the inner property class. *Further* exhaustive (wasted) experimentation found that outer class design time handling supports no more than the following (most simple possible) accessor form:
C# Example - Outer class Property Definition Pattern
[Browsable( true )]
[Category( "Appearance" )]
[Description( "Tints and/or adjusts the luminosity of system colors.
Derivatives survive distribution and OnSystemColorsChanged.
Default = 0, 0, 0." )]
[DesignerSerializationVisibility( DesignerSerializationVisibility.Content )]
public GCColorOffset ColorOffset
{
get
{
return f_ColorOffset;
}
set
{
f_ColorOffset = value;
}
}
Given these costly issues, a basic (albeit otherwise undesirable and illogical) pattern paves the way for your composite class design to succeed.
Pattern For Declaring DefaultAttributes
Firstly, even if this obstructs good and usual design intentions, DefaultValueAttributes
can only be declared on the properties of the inner property
class:
C# Example
[Browsable( true )]
[DefaultValue( 0 )]
[Description( "Determines offsetting (if any) of the R Color field.
Default = 0 (or none), of the range -255...255 inclusive." )]
public Int32 R
{
Revising Inner Classes to Fire Obligatory Outer Class Functions
Secondly, we must revise both property class and outer class designs so that vital accessor functions of the outer class are fired from the property
class.
This purpose is reasonably accomplished with delegates or references. Because this example requires the reference for other purposes which are not evident from this example, and because we do not have to expose this property class elsewhere, we first show how to use a reference to the outer class. To preempt potential calls to a null reference, we assign the reference as early as possible with a specialized constructor for the property class:
C# Example
protected internal GlossContourButton™ ReferenceToOuterClass;
public GCColorOffset( GlossContourButton™ RootRef )
: base( )
{
ReferenceToOuterClass = RootRef;
}
When the outer class creates an instance of the property class, of course it passes this
to the specialized property class constructor:
f_ColorOffset = new GCColorOffset( this );
Having called such a constructor so, accessors of our property class can make the calls into the outer class functions which the IDE ignored when we made them where they belong. We now call them (absurdly) from the outer class's set accessor:
[Browsable( true )]
[DefaultValue( 0 )]
[Description( "Determines offsetting (if any) of the R Color field.
Default = 0 (or none), of the range -255...255 inclusive." )]
public Int32 R
{
get
{
return f_R;
}
set
{
if ( value != f_R )
{
f_R = Rectify( value );
if ( null != ReferenceToOuterClass )
{
ReferenceToOuterClass.P__PrepSurfacesAndInvalidateConditionally( );
}
}
}
}
Alternatively, we could assign a delegated handler in such a constructor, calling the delegate method from our accessor:
[Browsable( true )]
[DefaultValue( 0 )]
[Description( "Determines offsetting (if any) of the R Color field.
Default = 0 (or none), of the range -255...255 inclusive." )]
public Int32 R
{
get
{
return f_R;
}
set
{
if ( value != f_R )
{
f_R = Rectify( value );
if ( null != ReferenceToDelegate )
{
ReferenceToDelegate( this, new EventArgs( ) );
}
}
}
}
Finished nested node behavior shows the resultant non-support for Reset
in the outer property declaration. Owing to the required DefaultValueAttribute
declarations, only the inner class property declarations (R
, G
, and B
) provide Reset
support.
This reference or delegate pattern solves our problems and demonstrates that there's just a bit more to every little composite property than might meet the eye. Of course, when/if the IDE is ultimately repaired, successful designs will be achieved as easily as they should have been from the beginning — eliminating the temporary need for these workarounds, and requiring that you revise your class designs (again) to your original intention.