Click here to Skip to main content
15,884,628 members
Articles / Programming Languages / C#
Article

Vital Techniques for Using Objects as Properties

Rate me:
Please Sign up or sign in to vote.
2.91/5 (8 votes)
7 Mar 20076 min read 35.2K   15   21
Explains critical undocumented design issues of building components with objects as properties

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.

GlossContourButton™.

This example exposes a ColorOffset property for a custom button class, producing the following nested node and reset behavior in the IDE.

Image 2

"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

C#
/// <summary>
/// GCColorOffsetTypeConverter provides expandable Property node 
/// interface functionality to GCColorOffset properties.
/// </summary>

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

C#
/// <summary>
/// Data structure for R, G, and B Color offsets.
/// </summary>

[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

C#
/// <summary>
/// Tints and/or adjusts the luminosity of system colors. 
/// Derivatives survive distribution and OnSystemColorsChanged. Default = 0, 0, 0.
/// </summary>
[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;

          /* VITAL ACCESSOR FUNCTIONS NOT FIRED.
          if ( ( value.f_B != f_ColorOffset.f_B ) || 
                ( value.f_G != f_ColorOffset.f_G ) || 
                    ( value.f_R != f_ColorOffset.f_R ) )
               {
               // ASSIGN THROUGH ACCESSORS TO RECTIFY VALUES.
               f_ColorOffset.B = value.f_B;
               f_ColorOffset.G = value.f_G;
               f_ColorOffset.R = value.f_R;

               Prep_Surfaces( );
               P__InvalidateConditionally( );
               } */
          }
}

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

C#
/// <summary>
/// Tints and/or adjusts the luminosity of system colors. 
/// Derivatives survive distribution and OnSystemColorsChanged. 
/// Default = 0, 0, 0.
/// </summary>
[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

C#
[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

C#
protected internal GlossContourButton™ ReferenceToOuterClass;
/// <summary>
/// Initializing constructor.
/// </summary>
/// <param name="RootRef">Required root control reference.</param>
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:

C#
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:

C#
/// <summary>
/// Determines offsetting (if any) of the R Color field. 
/// Default = 0 (or none), of the range -255...255 inclusive.
/// </summary>

[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( ); 
            // FUNCTION CALL SUCCEEDS.
                }
               }
          }
}

Alternatively, we could assign a delegated handler in such a constructor, calling the delegate method from our accessor:

C#
/// <summary>
/// Determines offsetting (if any) of the R Color field. 
/// Default = 0 (or none), of the range -255...255 inclusive.
/// </summary>
[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( ) ); 
                    // CALL SUCCEEDS.
                    }
               }
          }
     }

Image 3

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
C# and C++, "not necessarily in that order."

Comments and Discussions

 
Generalforget about objects Pin
reggingsucks@dodgeit.com1-Mar-07 9:47
reggingsucks@dodgeit.com1-Mar-07 9:47 
GeneralRe: forget about objects Pin
mike montagne1-Mar-07 10:37
mike montagne1-Mar-07 10:37 
GeneralRe: forget about objects Pin
NormDroid1-Mar-07 10:42
professionalNormDroid1-Mar-07 10:42 
GeneralRe: forget about objects Pin
mike montagne1-Mar-07 10:55
mike montagne1-Mar-07 10:55 
GeneralRe: forget about objects Pin
NormDroid1-Mar-07 20:57
professionalNormDroid1-Mar-07 20:57 
GeneralRe: forget about objects, YEP Pin
reggingsucks@dodgeit.com1-Mar-07 10:52
reggingsucks@dodgeit.com1-Mar-07 10:52 
GeneralRe: forget about objects, YEP Pin
Jamie Nordmeyer1-Mar-07 11:33
Jamie Nordmeyer1-Mar-07 11:33 
GeneralRe: forget about objects Pin
NormDroid1-Mar-07 10:41
professionalNormDroid1-Mar-07 10:41 
GeneralRe: forget about objects Pin
reggingsucks@dodgeit.com1-Mar-07 10:53
reggingsucks@dodgeit.com1-Mar-07 10:53 
GeneralRe: forget about objects Pin
mike montagne1-Mar-07 10:57
mike montagne1-Mar-07 10:57 
GeneralRe: forget about objects Pin
mike montagne1-Mar-07 10:59
mike montagne1-Mar-07 10:59 
GeneralRe: forget about objects Pin
Colin Angus Mackay2-Mar-07 4:43
Colin Angus Mackay2-Mar-07 4:43 
GeneralRe: forget about objects Pin
mike montagne2-Mar-07 5:48
mike montagne2-Mar-07 5:48 
GeneralRe: forget about objects Pin
Colin Angus Mackay2-Mar-07 6:44
Colin Angus Mackay2-Mar-07 6:44 
GeneralRe: forget about objects Pin
mike montagne2-Mar-07 7:55
mike montagne2-Mar-07 7:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.