Saturday, February 24, 2007

How to add a "published" property without breaking DCU compatibility

First off, if you are an OOP or framework purist and have a weak stomach, you can stop reading now.  In the upcoming Delphi 2007 for Win32, we took an unprecidented approach to adding functionality to the VCL.  If you've used Delphi for more than a couple of versions, you can fully appreciate that when the VCL framework changes, you have to also get updates to your third-party components.  Now I'm not going to discuss the reasons why, since I've covered this in detail over the years as to why this happens.  Basically it boils down to having the flexibility to make changes to existing classes and the compiler itself as we see fit to provide the best overall experience that we can.  So, for all of you who will make the knee-jerk comments about “why don't you just do this for every release!”  Had we done that, you'd never have packages, interfaces, class variables, inline functions, and in a post-Delphi 2007 release, generics.  Just browse through some of my past posts for more details.

With that said, we decided to take a slightly different approach for Delphi 2007.  D2007, is being touted as a “non-breaking” release.  What exactly does this mean?  In a nutshell, it simply means that you should be able to take most of your components for BDS2006 and install them into D2007.  I say most because there will, of course, be some fringe cases that this isn't possible.  For example, if you have a component package that likes to reach around the existing design-time interfaces and muck with things behind the scenes, that is out of the scope of the non-breaking change.  As long as your existing component design-time package sticks with the existing published interfaces it should work without a recompile.

Uh... so what's new then??  It is true that a lot of the changes we've made have been in the “implementation” section only and so there is no DCU breakage there.  But there are also quite a few changes that manifest as either whole new components or in one case a brand-new published property on TForm!  The rules for type compatibility with the Delphi compiler is deceptively simple.  Basically, for any structured type (class, record, object, etc...), the “version” of that type or symbol is derived from all the embedded symbols and types.  So you cannot modify an existing class type or method declaration without causing this “version” to change.  This is the cause of the error message “xxx” was compiled with a different version of “yyy.”

For the astute among you that have been following along so far, you are probably already jumping ahead here and thinking.. of course, class helpers!  Well, yes, you'd be right on that count, to a point.  I did say that the new property is “published” which means it also shows up in the Object Inspector in the IDE.  So there is a little more gong on than just a class helper.  As it turns out, using a class helper will make this new property appear from the user's source code to be on  TCustomForm, which is the desired effect.  So we just added a property without actually touching the original TCustomForm declaration.  Then there is the streaming and the storage of the property in the instance.  This is where you should avert your eyes if you're a little queasy ;-).  We simply take and overload an existing, little-used, private field on the TCustomForm instance and create an internal (implementation section only) structure that has a field of the type of this overloaded field to now hold its value along with whatever fields are going to store the class helper's property data.  Then we go through all the places in Forms.pas that access this field and to some casting tricks to use that field as a pointer to that structre.  Since it is a private field, there is no effect of any descendants outside the Forms.pas unit.  This is actually a place where the OOP tennet of encapsulation helped us acheive this.  We were able to make this change and no code or classes that use an instance of this class are any more the wiser.

That takes care of the storage of the data behind the scenes. Now how about streaming this data since I did actually say that this was suppose to be a “published” property?  This is someplace where some past changes to the framework helped us.  TCustomForm had already overridden the DefineProperties method, so now it was a simple matter of adding some more code that method to handle the streaming of the property data.  This is also where the class helper came in handy because we could put the read and write methods on it.  Again, without changing the TCustomForm class.  So now we have a new property on the TCustomForm class, the streaming is all hooked up and you can now access this property on any TCustomForm or decendant instance.  The final piece now is how to get this property to show up in the Object Inspector.

This is where the ISelectionPropertyFilter interface comes in.  You may or may not have seen this little one-method interface that was introduced in BDS2006 in the DesignIntf unit.  For those of you with BDS2006, you can open DesignIntf.pas and read the usage comments there about where and how to implement this interface.

  { ISelectionPropertyFilter
    This optional interface is implemented on the same class that implements
    ISelectionEditor.  If this interface is implemented, when the property list
    is constructed for a given selection, it is also passed through all the various
    implementations of this interface on the selected selection editors.  From here
    the list of properties can be modified to add or remove properties from the list.
    If properties are added, then it is the responsibility of the implementor to
    properly construct an appropriate implementation of the IProperty interface.
    Since an added "property" will typically *not* be available via the normal RTTI
    mechanisms, it is the implementor's responsibility to make sure that the property
    editor overrides those methods that would normally access the RTTI for the
    selected objects.

        Once the list of properties has been gathered and before they are sent to the
        Object Inspector, this method is called with the list of properties.  You may
        manupulate this list in any way you see fit, however, remember that another
        selection editor *may* have already modified the list.  You are not guaranteed
        to have the original list.

  ISelectionPropertyFilter = interface
    procedure FilterProperties(const ASelection: IDesignerSelections;
      const ASelectionProperties: IInterfaceList);

So the basic trick is to register a selection editor for TCustomForm that implements this inteface.  When FilterProperties is called, we can now completely manipulate the whole list of IProperty interfaces in the ASelectionProperties argument.  You can add and remove properties.  For instance, if you drop a TFlowPanel on a form, then drop components onto it, you'll notice that the embedded components don't have a Left and a Top property.  Not because the component doesn't have them, but because they are useless at design-time so they get removed from the ASelectionProperties list.  In the TCustomForm case we actually want to add a property.  So we search through the list of properties, find the insertion point, instantiate a class that implements IProperty.  Now your “published” property shows up in the Object Inspector!

While this all seems like a lot of work to go through just to maintain compatibility, the good news is that in a post Delphi 2007 release we can fold these changes back into the TCustomForm class, delete the class helper and the ISelectionPropertyFilter, and the existing DFM files will work just fine.  So will all your source code that accesses these new properties.  One interesting thing is that this proof-of-concept could lead to being able to add some significant functionality into interim releases without breaking existing components/DCUs.  It could even be that every other release is a breaking release.  Just some things for us to consider for the future.