Customizing Code Generation in the .NET Framework Visual Designers

 

Shawn Burke
Microsoft Corporation

March 2002

Summary: Discusses the various types of code generation and shows how component authors can participate in the code generation process in Microsoft .NET. (17 printed pages)

Download Buttonarraysample.exe.

Contents

Introduction
Basic Code Generation Concepts
Generation of Nested Objects
Generating Code for Custom Types
Using Binary Serialization
Making Components Aware of Initialization
Serializing Design-Time-specific Information
Controlling Code Generation
Conclusion

Introduction

When developing the design-time architecture for the Microsoft® .NET Framework, Microsoft chose to use source code as the persistence mechanism for user code rather than a binary or other private solution.

Code persistence allows users to learn from the code that the Designer outputs as well as be able to easily build projects outside of the Microsoft Visual Studio® .NET environment if desired. It also allows understandable and accessible customization for advanced scenarios and components.

For the majority of cases, the Designer can automatically generate the right code to persist the state of custom components. However, there are cases where a control author needs to supply more information to the code generation engine or may wish to customize how it treats a given type. This article discusses the various types of code generation and shows how component authors can participate in the code generation process.

Basic Code Generation Concepts

In a nutshell, code generation works by inspecting the public read/write properties of an object and outputting each value to code (or to a resource file if they can't be represented in code). As a simple example, consider a Button on a Form. The C# code generated for that button would look something like this:

// 
   // button1
   // 
   this.button1 = new System.Windows.Forms.Button();
   this.button1.Location = new System.Drawing.Point(40, 48);
   this.button1.Size = new System.Drawing.Size(104, 48);         
   this.button1.Text = "OK";

Clearly, the Button class has more properties than just "Location," "Size," and "Text." But on the other hand, code generation doesn't want to generate code for any more properties than it has to for many reasons such as performance of loading and initializing, both at design time and run time. So, there are two primary methods of suppressing code generation for an object. The first is by applying the System.ComponentModel.DesignerSerializationVisiblityAttribute to the property. By default, a public read/write property will have code generated for it, unless the DesignerSerializationVisiblity.Hidden value is placed on it.

[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
   public string StringProp 
   {
      get 
      {
         return stringPropValue;
      }
      set 
      {
         stringPropValue = value;
      }
   }   

Applying this attribute will prevent code from ever being generated for this property. However, it is usually the case that you want to prevent the default values from being generated for a property. There are two methods for accomplishing this. The first is by applying the System.ComponentModel.DefaultValueAttribute. DefaultValueAttribute takes a value that describes the default value for a property. If the current value of a property matches that default value, code is not generated.

private string text = "OK";

      [DefaultValue("OK")]
      public string Text 
      { 
         get 
         {
            return this.text;
         }
         set 
         {
            this.text = value;
            Invalidate();
         }
      }

But what if the default value is not a simple one or this needs to be done more dynamically? This is often the case for values that are inherited from other components such as a parent Control. The dynamic case can be handled by adding a method of a certain signature. Using the example above, the code would look like this:

private bool ShouldSerializeText() 
      {
         return Text != "OK";
      }

By adding a method of the name ShouldSerialize<Property Name>, the code generation engine can ask the component if it would like the value to be serialized. Notice the ShouldSerialize method can be made private so it won't clutter up your object model. There are several examples of this in the attached ButtonArray sample.

Generation of Nested Objects

Not all of the state of an object is available on its top level properties. Many objects have properties that return a reference to a nested object, such as collection properties. But almost all types in the .NET Framework, from Object to Control have public properties on them. So how do you mark a property as one that the code generation engine should treat differently?

The answer here is another value for the DesignerSerializationVisiblityAttribute that we have already discussed. In this case, you mark a property with DesignerSerializationVisiblity.Content to indicate the code generator should "walk in" to this property and generate code for it. For example, this is the code for the DockPadding property on a Form:

this.DockPadding.Left = 5;
   this.DockPadding.Top = 4;
   

If you look at the Framework, you will see a class called System.Windows.Forms.ScrollableControl.DockPaddingEdges. The ScollableControl class has a property of this type, and internally contains an instance of it. The property itself is actually read only—you can not set in a new instance of DockPaddingEdges. Specifying DesignerSerializationVisiblity.Content causes the code generation to recurse on the DockPadding property's value and generate the code seen above.

Generating Code for Custom Types

Consider the code that is generated for a TreeView with some nodes in it:

this.treeView1 = new System.Windows.Forms.TreeView();
this.treeView1.Location = new System.Drawing.Point(72, 104);
this.treeView1.Name = "treeView1";
this.treeView1.Nodes.AddRange(
new System.Windows.Forms.TreeNode[] {
      new System.Windows.Forms.TreeNode("Node0"),
      new System.Windows.Forms.TreeNode("Node1")});
                  

Notice that the code that is generated for the sub items inline, and their constructors have the text for the label passed into them. If you look at the TreeNode class, you'll notice that is has quite a few constructors. It has five of them to be exact. So how does the code generator know which one to call? Enter System.ComponentModel.Design.InstanceDescriptor to save the day. InstanceDescriptor tells the code generation engine which method (constructor or otherwise) to call when creating a class. The InstanceDescriptor for a given type is retrieved from that type's TypeConverter. A TypeConverter with this knowledge is able to "convert" a value to the InstanceDescriptor type in the same way it converts the value to other types such as string.

Essentially what an InstanceDescriptor does is provide the code generation engine with three pieces of information:

  1. What member (usually a constructor, but it could be a static method) should be invoked to get an instance of an object?
  2. What values should be passed to that member to initialize the object's state?
  3. Does the invocation completely describe the state of the object or is more persistence necessary?

The TreeNode samples above are an example of code that is completely described by the constructor. No other property sets are necessary to properly initialize the TreeNodes. But if sub-objects have a great number of properties themselves, it is often difficult or impractical to have all the permutations of constructors that are necessary to always accomplish this. Here is an example of a sub-item that is not completely initialized by its constructor:

new System.Windows.Forms.ListViewItem listViewItem1 = 
new ListViewItem(new string[] { "a"}, 
-1, 
System.Drawing.Color.Empty, 
System.Drawing.Color.Red, 
null);
this.listView1 = new System.Windows.Forms.ListView();         
listViewItem1.Checked = true;
listViewItem1.StateImageIndex = 1;
this.listView1.Name = "listView1";
this.listView1.Items.AddRange(
new System.Windows.Forms.ListViewItem[] {listViewItem1});
         
         

Notice there are lines of code generated for "listViewItem1" inline with those for "listItem1". The code generator uses the constructor that is specified, but will still generate code for other properties that have modified values. InstanceDescriptor manages this process.

The InstanceDescriptor class itself is simple. It is composed of a System.Reflection.MemberInfo, a Boolean describing if the member invocation completely describes the object, and an array of objects to serialize as the parameters. As mentioned above, the way to associate an InstanceDescriptor with a class is through a TypeDescriptor. For example, the InstanceDescriptor for a System.Drawing.Point looks something like this:

public class PointConverter : TypeConverter 
{
   public override bool CanConvertTo(
ITypeDescriptorContext context, 
Type destinationType) 
   {
      if (destinationType == typeof(InstanceDescriptor)) 
         return true;
      return base.CanConvertTo(context, destinationType);
   }

public override object ConvertTo(
ITypeDescriptorContext context, 
CultureInfo culture, 
object value, 
Type destinationType) 
{
   // other TypeConverter operations go here...
   //
   if (destinationType == typeof(InstanceDescriptor) && 
value is Point) 
   {
      Point pt = (Point)value;

      ConstructorInfo ctor = 
typeof(Point).GetConstructor(
new Type[] {typeof(int), typeof(int)});
      if (ctor != null) 
      {
         return new 
InstanceDescriptor(
ctor, 
new object[] {pt.X, pt.Y});
}
}
      return base.ConvertTo(context, culture, value, destinationType);      
}

The code above is simple. It just selects the constructor on Point that takes two integers, and then creates an InstanceDescriptor that holds those values to be used as the initialization arguments. The attached sample code has another example of utilizing an InstanceDescriptor.

Using Binary Serialization

Not all types lend themselves cleanly to code generation. For example, how would you serialize a bitmap into code? You could encode it as a string, but for even a moderately sized bitmap the string could be much larger than the rest of the code in the file. The Designer solves this problem by persisting binary types into the resources file that pushes the values into the manifest for the .NET Framework application. Then, when the application is initialized, it can load the values back out of the Assembly's manifest. For example, this is the code that is generated if your form has an image on the background.

System.Resources.ResourceManager resources = 
new System.Resources.ResourceManager(typeof(form3));

this.BackgroundImage = 
   ((System.Drawing.Image)
(resources.GetObject("$this.BackgroundImage")));
this.ClientSize = new System.Drawing.Size(292, 273);
this.Name = "form3";
this.Text = "Form2";

As you can see, the values are stored somewhere other than in code. This makes sense for many data types that would be difficult to serialize solely as generated code. The code generator follows this order when trying to serialize a type:

  1. Built in serialization of primitive types (string, bool, integer, float, etc.)
  2. InstanceDescriptor serialization for custom types
  3. Binary serialization for types that are marked as Serializable.

But how can a type be "marked as Serializable"? This can be accomplished in two ways. The simplest is to just add the System.SerializableAttribute to your class and allow the automatic serialization to serialize all the field values within your class. If there are fields that you don't want serialized (such as handles or other dynamic information), you can mark those fields with System.NonSerializedAttribute. Below is a very simple example:

[Serializable]
   public class SomeClass 
   {
      private string s;
      private int    i;
      [NonSerialized]
      private IntPtr handle;
   }

The second way to mark a class as Serializable is to implement ISerializable. This gives the class author more control over how a class is Serialized. Note the special constructor signature for deserialization, which can be private.

public class SomeClass : ISerializable
{
   private string s;
   private int    i;
   private IntPtr handle;
   public SomeClass(string s, int i) 
   {
   }

   private SomeClass(
SerializationInfo info,
SerializationContext context) 
   {
      // Serialization with ISerializable expects
      // a constructor like this to deserialize
      // an object.
      // We know what we stored below,
      // so fetch those values
      s = info.GetString("stringValue");
      i = info.GetInt32("intValue");
   }

   void ISerializable.GetObjectData(
      SerializationInfo info,
      StreamingContext context
      ) 
   {
      // push the values we want to store
      // into the SerializationInfo
      info.AddValue("stringValue", s);
      info.AddValue("intValue", i);
   }
}

By implementing one of the two ways above, your class will be saved as a binary resource format, and the code generator will generate the proper code to initialize its value from the Assembly's resources.

Making Components Aware of Initialization

When writing managed components, the rule of thumb is to keep them modeless with respect to property sets. What that means is that users should be able to set properties in any order on an object and get the same results. For example, on a Control, a user can set the Text, the Size, the BackColor, and then the Visibility. Or the BackColor, the Visibility, the Size, and then the Text and get the same result. There are a very few cases when this is not possible—for example if a property's value depends on the value of another property but it is a general rule that should be followed.

More often, however, there are cases of interaction between properties that you want to be enforced when a user is coding against your component but are difficult at design time. Imagine a slider control that allows a user to select a value by moving an indicator between a minimum and maximum value. And say the default for minimum is 0 and maximum is 10.

In the Designer, a user sets minimum to -100, maximum to -1 and the current value to -50. At runtime, when the value property is set, the control would throw an exception if the value is not between the minimum and maximum values. However, when code is generated, there is no guarantee as to what order the property sets will be performed in. If the maximum value happens to be set before the minimum value, throwing an exception would not be appropriate from within the initialization code. The System.Windows.Forms.TrackBar control confronts exactly this problem.

To solve this problem, you class can implement the System.ComponentModel.ISupportInitialize interface. This interface is simple. It has two methods: BeginInit, and EndInit. These methods are called, naturally, when initialization starts and ends, respectively. When the code generator generates code for an ISupportInitialize object, it will look something like this:

this.trackBar1 = new System.Windows.Forms.TrackBar();
((System.ComponentModel.ISupportInitialize)
            (this.trackBar1)).BeginInit();
this.trackBar1.Maximum = 50;
this.trackBar1.Minimum = 10;
this.trackBar1.Name = "trackBar1";
this.trackBar1.Value = 25;
((System.ComponentModel.ISupportInitialize)(this.trackBar1)).EndInit();

Internally, the TrackBar sets a flag that reminds it not to validate values when initializing. When EndEdit is called, it does a batch validation on its settings to make sure the values that were passed in are valid.

Serializing Design-Time-specific Information

When your component is on the design surface, it may be useful to persist extra information about the component that is only relevant at design time. For example, the Form has a property that determines the size of the snap-to grid that is overlaid onto the form called "GridSize". The Form class itself has no "GridSize" property and therefore this data cannot be saved into the generated code. The way to handle this is with "design time properties".

Generally, design time properties do not correspond to an actual property on the component. Instead, these properties are added by the Designer itself. To prevent these properties from being generated into the code file, the System.ComponentModel.DesignOnlyAttribute can be applied. The values for any properties with this attribute will be serialized into the Form's design time resource (.resx) file rather than into code. For more information on Designers, see Writing Custom Designers for .NET Components.

An example design time properties also exists in the attached ButtonArray sample.

internal class ButtonArrayDesigner : ControlDesigner
{
   private bool shadeButtons = true;

   public ButtonArrayDesigner()
   {         
   }

   // The "DesignOnly" attribute will cause this property
   // to be persisted into the resources file rather
   // than the code stream.
//
   [DesignOnly(true), Category("Design"), DefaultValue(true)]
   public bool ShadeButtons 
   {
      get 
      {
         return shadeButtons;
      }
      set 
      {
         if (shadeButtons != value) 
         {
            shadeButtons = value;
            Control.Invalidate();
         }
      }
   }
   protected override void 
      PreFilterProperties(IDictionary properties) 
   {
      // create and add the ShadeButtons property
      //
      PropertyDescriptor shadeProp = 
         TypeDescriptor.CreateProperty(
typeof(ButtonArrayDesigner), 
"ShadeButtons", 
typeof(bool));
      properties[shadeProp.Name] = shadeProp;
      base.PostFilterProperties(properties);
   }
//…
}

Controlling Code Generation

But what happens if you need special methods called on your object during initialization? When you add a container Microsoft Windows® Forms control to your project, you may notice that methods are called like the following:

private void InitializeComponent()
{
   this.button1 = new System.Windows.Forms.Button();
   this.SuspendLayout();
   // 
   // button1
   // 
   this.button1.Location = new System.Drawing.Point(128, 96);
   this.button1.Text = "button1";
   // 
   // form1
   // 
   this.Controls.AddRange(new System.Windows.Forms.Control[] {
 this.button1});
   this.Name = "form1";
   this.Text = "Form1";
   this.ResumeLayout(false);
}

Notice the "SuspendLayout" and "ResumeLayout" calls that are made on the form. When you add a Panel with child controls to the form, the Panel will also have a SuspendLayout call and a matching ResumeLayout call. How is this accomplished?

Code Generation by the Visual Studio .NET Designer is done by first generating what is referred to as a "CodeDom Tree." CodeDom, which is an abbreviation of "Code Document Object Model", is a language-agnostic way of expressing code that can then be handed off to a language-specific code generator. This is very similar to what some people may refer to this as an "Abstract Syntax Tree". So, one can build a set of statements using CodeDom and then hand them to a Microsoft Visual Basic® .NET, C#, Microsoft JScript® .NET, or any .NET Language code generator to generate the appropriate code for that language. It is in this way that the Designer can avoid having specific knowledge of languages and can therefore conceivably generate code in any .NET language. There are several CodeDom articles available in the .NET Framework documentation and on MSDN for more information.

But as a quick example, let's look at how you would generate the following simple Visual Basic .NET code with the CodeDom.

Dim i As Integer = 10

If (i = 10) Then
           i = 5
      End If

First, you would need to build the statements. This can be verbose but once you understand it, it's relatively simple to build more complex statement trees.

' first build the declaration (Dim I as Integer = 10)
'
Dim statements As CodeStatementCollection = New CodeStatementCollection
Dim variable As CodeVariableDeclarationStatement = 
New CodeVariableDeclarationStatement(
GetType(Integer), "i", New CodePrimitiveExpression(10))
Dim variableRef As CodeVariableReferenceExpression = New 
CodeVariableReferenceExpression("i")
statements.Add(variable)

' now build the assignment inside the if (i = 5)
'
Dim assign As CodeAssignStatement = New CodeAssignStatement
assign.Left = variableRef
assign.Right = New CodePrimitiveExpression(5)

' finally build the if statement
'
Dim ifStatement As CodeConditionStatement = New CodeConditionStatement

' build the condition, which is a binary expression of type '='
'
ifStatement.Condition = New CodeBinaryOperatorExpression(
variableRef, 
CodeBinaryOperatorType.ValueEquality, 
New CodePrimitiveExpression(10))

' add the statements to be executed if the condition results in true
'
ifStatement.TrueStatements.Add(assign)
statements.Add(ifStatement)

Now that we've got the expression built, it's time to generate the code:

Dim provider As CodeDomProvider
Dim generateVB As Boolean = True

' create the appropriate generator
'
If (generateVB) Then
    provider = New Microsoft.VisualBasic.VBCodeProvider
Else
    provider = New Microsoft.CSharp.CSharpCodeProvider
End If

Dim generator As ICodeGenerator = provider.CreateGenerator()

' now walk the statement collection we created above and 
' output each line of code to the console standard output.
'
For Each s As CodeStatement In statements
     generator.GenerateCodeFromStatement(s, Console.Out, Nothing)
Next s

Now that we've completed our "crash course on CodeDom," we can look at how to customize the generation of code for a component. Each component class has associated with it a Serializer, using the System.ComponentModel.Design.Serialization.DesignerSerializer attribute. The Framework has built in serializers for most basic types, but there is a hook in place for component authors to participate in this process. Serializers can do anything they want, but the Designer generally works with serializers that are based on System.ComponentModel.Design.Serialization.CodeDomSerializer. To modify the serialization process, one would derive from this class. There are, however, serializers that do not derive from CodeDomSerializer. For example, the serializers that are responsible for moving values in and out of the Designer resources (the .resx file) are not CodeDomSerializers, so it is reasonable to write a serializer that outputs information in a non-CodeDom way. But it is important to remember that serializers only function at design time.

As an example, the attached ButtonArray sample adds two bits of code to its code generation, highlighted below in bold. First, it adds some dynamic information to the comment that is generated above the ButtonArray code. Second, it adds a custom method call after it has generated the properties for the ButtonArray.

'This is a custom comment added by a custom serializer on Thursday, 
'February 28, 2002
'
'ButtonArray1
'
Me.ButtonArray1.Location = New System.Drawing.Point(64, 32)
Me.ButtonArray1.Name = "ButtonArray1"
Me.ButtonArray1.Size = New System.Drawing.Size(112, 112)
Me.ButtonArray1.CustomMethod()

Both of these are generated using CodeDom. The first step is associating a custom CodeDomSerializer with the ButtonArray class using the DesignerSerializerAttriubute.

[DesignerSerializer(typeof(ButtonArraySerializer), 
   typeof(CodeDomSerializer))]
public class ButtonArray : System.Windows.Forms.UserControl, 
                     ISupportInitialize { //… 
}

Second, we implement our serializer. See the attached sample for the full code, but it is important to note that we always ask the serialization manager for the "base" serializer and invoke that for default serialization. That is a very important step; without it the correct code will not be serialized. Also, since we are only generating simple code, the base CodeDomSerializer can handle the deserialization for us, so we will only do work in the Serialize method:

/// <summary>
/// The custom serializer for the ButtonArray type that adds
/// some simple, sample output to the code that is generated for the
/// ButtonArray
/// </summary>
public class ButtonArraySerializer : CodeDomSerializer
{

      // See sample for the Deserialize method

/// <summary>
/// We customize the output from the default serializer here, adding
/// a comment and an extra line of code.
/// </summary>
public override object Serialize(
IDesignerSerializationManager manager, 
object value) 
{
// first, locate and invoke the default serializer for 
// the ButtonArray's  base class (UserControl)
//
CodeDomSerializer baseSerializer = 
(CodeDomSerializer)manager.GetSerializer(
typeof(ButtonArray).BaseType, 
typeof(CodeDomSerializer));

   object codeObject = baseSerializer.Serialize(manager, value);

   // now add some custom code
   //
   if (codeObject is CodeStatementCollection) 
   {

      // add a custom comment to the code.
      //
      CodeStatementCollection statements = 
(CodeStatementCollection)codeObject;   
      statements.Insert(0, 
new CodeCommentStatement(
"This is a custom comment added by a custom serializer on " + 
               DateTime.Now.ToLongDateString()));

      // call a custom method.
      //
      CodeExpression targetObject = 
         base.SerializeToReferenceExpression(manager, value);
         if (targetObject != null) 
         {
            CodeMethodInvokeExpression methodCall = 
new CodeMethodInvokeExpression(targetObject, "CustomMethod");
            statements.Add(methodCall);
         }
         
      }

      // finally, return the statements that have been created
      return codeObject;
   }
}

When that serializer runs, it will generate the sample code above, or the equivalent code in C#. This method is exactly how Windows Forms container controls add the function calls for SupendLayout and ResumeLayout to their code generation.

Conclusion

The .NET Framework Visual Designers in Visual Studio .NET have a powerful and flexible code generation built in. For most components, code generation will handle their persistence properly with little or no assistance from the component author. But for more advanced scenarios or specific needs, the code generation does expose straightforward extensibility to cover those scenarios for which the default handling does not meet everyone's needs.