Dynamic Properties and Code Generation
Michael has did to say about my code-generation work.
Interesting approach for code generation using XML. I think there are some disadvantages though. The whole system becomes more complex and code generator takes time to run. But this is the cost of additional abstraction layer...
Sometimes, the programming language we used doesn't provide a concise way of expressing code. For example, the CLR does not provide a dynamic property mechanism. Developers must instead implement an internal properties dictionary and wrap the property accessors around accesses to that dictionary.
In WinForms, the designers opted for the following scheme when adding properties, such as IsAnimated, to a control. The WinForms control must add a new property get and set accesor for the property. However, each control use a dynamic property scheme to its properties, rather than using a simple variable as a backing store for several reasons.
- Unset state. Some properties, called ambient properties, inherit their properties from parent controls, if they are unset--this is very similar to style sheets. There properties include Font, BackColor, Cursor, and so on. Therefore, a property must know if it is unset--which is a problem that Nullable<T> attempts to address.
- Compactness. Controls store a large number of properties and events, most of which are rarely set. In the common case, none of the properties or events are actually set.
So, Controls maintain an internal dictionary, which we will call "Properties" for storing events and properties. To identify the property, a new key is required for comparison.
private static object IsAnimateKey = new object[];
A string may do, but an object is more compact and uses fast reference-based equality. Next a property accessor is defined. The property may have certain attributes such as CategoryAttribute and DefaultValueAttribute that need to be set to interoperate better with the designer. A constant for the default value, to be return by the get accessor if the property is unset, should be defined for maintenability. When the property is set, an change event must be fired.
private const DefaultIsAnimatedValue = false;
[Category("Appearance")]
[DefaultValue(DefaultIsAnimatedValue)]
public bool IsAnimated
{
get
{
object result = this.Properties[IsAnimatedKey];
if (result == null)
return DefaultIsAnimatedValue;
return (bool) result;
}
set
{
if (value == this.Properties[IsAnimatedKey]
this.Properties[IsAnimatedKey] = value;
OnIsAnimatedChanged(EventArgs.Empty)
}
}
If IsAnimated were an ambient property, then we would return Parent.IsAnimated, or DefaultAnimatedValue, if Parent is null.
Controls provide both a inheritance mechanism (via virtual protected instance methods) and an observer pattern (via events) for each property changed event. We must write these out as well. We start by creating a new key for the event handler that we store in the property dictionary.
private static object IsAnimatedChangedKey = new object[];
protected virtual OnIsAnimatedChanged(EventArgs args)
{
EventHandler handler = this.Properties[IsAnimatedChangedKey];
if (handler != null)
handler(this, args);
}
public event EventHandler IsAnimatedChanged
{
add { this.Properties[IsAnimatedChangedKey] =
Delegate.Combine((Delegate)this.Properties[IsAnimatedChangedKey], value); }
remove { this.Properties[IsAnimatedChangedKey] =
Delegate.Remove((Delegate)this.Properties[IsAnimatedChangedKey], value); }
}
Some properties, especially in Whidbey, include a reset method as well.
public void ResetIsAnimated()
{
this.IsAnimated = DefaultIsAnimatedValue;
}
Avalon use dependency properties , a form of dynamic properties, for its controls, which are almost as cumbersome to create as standard WinForms properties above. Properties need to be registered and cached. Callbacks need to be defined for each property. The good news is that, once created, a lot of services are provided for free, such as stylesheets, undo support, serialization, redrawing, change notification, data binding, validation, inheritance, and so on.
Now here's the benefit of code generation.
I can write the code for the IsAnimated property above, and the equivalent two other properties, BackgroundColor and Opacity in the compact XML file below.
<?xml version="1.0"?>
<properties class="MyControl" namespace="MyCompany">
<property name="IsAnimated" type="bool" defaultvalue="false"/>
<property name="Opacity" type="float" defaultvalue="1" minvalue="0" maxvalue"1"/>
<property name="BackgroundColor" type="Color" defaultvalue="Color.Black" />
</properties>
With an simple script in Perl or Python, I can automatically spit out several lines of implementation code for each line of XML in seconds. With XML, I can create clean wrappers around ugly mess such as the code above; I can generate highly efficient code that would ordinarily be painful to enter by hand.
Currently, half of my code is generated. I don't even use the new C# snippets (or expansions) feature, as I no longer write repetitious code.
My whole user interface is written in XML. I never saw the point of using the designer to manually worry about accelerators, alignment, tab order, layout, and other concerns that could automatically be resolved by the computer. I write my enum and interface classes in XML, allowing me to obtain enhanced enum support as well as default implementation interfaces. I have my own version of dynamic properties, which include automatic support for undo, validation, serialization, change notification, and numerous other features. This support would normally be tedious to add by hand. Essentially, I am creating a whole new language to make up for inadequacies in the feature set of the compiler and the runtime.
Michael was afraid that the system becomes more complex. However, I found that I have fewer source files to worry about. Development is simpler, since I merely make high-level modifications to my XML files. Testing is simpler, because there is less to test. Bugs are easier to find and fix. For example, in my properties example above, bugs in one property are exposed in every other property because they share the exact same generated implementation. Fixing a bug involves fixing the generator, which, when fixed, emits correct code for all the properties.
I use to suffer from code paralysis, because I could not decide between competing design. Now, I just enter the high-level features of the code into XML and my generators spits out code in seconds with one click. I can quickly produce multiple implementations and compare the results, just by changing the generator.
This looks very much like something I experimented with a couple of years ago. My idea was to have a language expressed in XML to make building code editors and designers easier. I wrote an XSLT stylesheet to convert it to C# code for compilation.
I'll dig out what I wrote and post it to my blog later.
Posted by: Graeme Foster | August 28, 2004 at 11:28 AM
Here we are. Not as close to what you were saying, but I still think it was ahead of its time ;)
http://graemef.com/?q=node/view/350
Posted by: Graeme Foster | August 28, 2004 at 12:22 PM
This may be quite the long shot, but maybe you could share some of that code gen tool with us? If not, how about just several of the ideas behind it?
I have thought about doing this quite a few times, but kept postponing it due to its vastness.
Posted by: Omer van Kloeten | August 28, 2004 at 01:31 PM
Right now, my tool is not ready for public consumption... It's a simple perl scripts. It is not quite reusable, since it consists of many hard-coded assumptions about the development environment, but I'll probably create a general purpose version sometime in the future, perhaps some task for Msbuild or Nant.
I haven't checked out any of the commercially available Code Generation tools, so I don't know how much better they are than my own.
Posted by: Wesner Moise | August 28, 2004 at 11:39 PM
You certainly are making good use of code generation. This strikes me as an example of the same phenomenon represented by formal unit testing. Such good development practices must manifest themselves in the discipline and ingenuity of the best developers early on, but as tools emerge to simplify and automate these tasks, they spread to the entire developer community.
Posted by: Nils Jonsson | August 30, 2004 at 08:53 AM
Well, I think you are right in general. This aproach is very good and agile.
One comment:
>Bugs are easier to find and fix. For example, in >my properties example above, bugs in one >property are exposed in every other property >because they share the exact same generated >implementation.
This is true, but these bugs are more serious in fact. One bug in code generator transforms into dozen bugs in application.
Michael
http://blog.targetprocess.com
Posted by: Michael | September 03, 2004 at 07:54 AM
What becomes more interesting is when you wish users to add properties to your types at run-time. We are solving this by using CodeDom to generated assemblies at run-time.
Posted by: Mark Sargent | November 17, 2004 at 06:11 AM