Posted by: Brian Pepin | June 20, 2008

Using Visual Studio Whidbey to Design Abstract Forms

A long time ago I posted an article explaining why Visual Studio can’t design abstract forms. I also promised that I’d show you a way you could make it work in Whidbey using Whidbey’s type description provider mechanism. Well, a long time has passed and I never wrote the follow-up. It’s time I fulfill my promise.

Type Description Providers

The heart of my mechanism is a custom TypeDescriptionProvider. I need to give you a little background on this new and powerful way of providing metadata to designers. First, while Visual Studio does use reflection to get to the properties and events on objects, it never uses System.Reflection directly. Instead, it uses a class called TypeDescriptor which is found in the System.ComponentModel namespace. In Visual Studio 7.0 and 7.1, TypeDescriptor was built on top of System.Reflection. It provided a simpler API for designers t use, and it also provided a simple extensibility mechanism through the ICustomTypeDescriptor interface.

We have completely redesigned TypeDescriptor’s internals for Visual Studio Whidbey. Instead of being built on reflection, TypeDescriptor is now built on TypeDescriptionProviders. There is a default TypeDescriptionProvider that uses reflection, but anyone can attach a provider to any class or instance they choose. This provides you with a huge increase in flexibility, as you now have control over the implementation of the entire TypeDescriptor API.

I’m going to write a custom TypeDescriptionProvider for my abstract for that takes some “creative liberties” with type metadata.

The Abstract Form

In my example, I have an abstract form called, not surprisingly, “AbstractForm”. It also defines a single abstract property called AbstractProperty:

public abstract class AbstractForm : Form {
    public abstract string AbstractProperty { get; set; }
}

It is impossible to create an instance of this class unless I derive from it and implement the abstract property. So, I have a class that does that too:

internal class ConcreteForm : AbstractForm {
    string _abstractProperty;

    public override string AbstractProperty {
        get { return _abstractProperty; }
        set { _abstractProperty = value; }
    }
}

My goal is to make things that derive from AbstractForm really look like they’re deriving from ConcreteForm. My first step in doing this is to put an attribute on AbstractForm that declares that it, and all classes that derive from it, should receive a custom type description provider:

[TypeDescriptionProvider(typeof(ConcreteClassProvider))]
public abstract class AbstractForm : Form {…}

Now I need to write a ConcreteClassProvider that does all of the dirty work. I’ll cover that in the next section.

ConcreteClassProvider

When I associated my type description provider to AbstractForm above, what I’m telling TypeDescriptor is that I know how to supply all metadata for AbstractForm and all of its derived types. If I had to write all of that logic myself it would be very complex and I probably wouldn’t get it right. Luckily, TypeDescriptionProvider has a constructor that takes an existing provider and delegates to it. By using this constructor all I have to do is override the areas I’m interested in and the rest will be taken care of for me. So, let’s dig in and write the skeleton for ConcreteClassProvider:

internal class ConcreteClassProvider : TypeDescriptionProvider {
    public ConcreteClassProvider() :
        base(TypeDescriptor.GetProvider(typeof(AbstractForm))) {
    }
}

There are a couple of noteworthy things about this code:

  1. It is internal, but its constructor is public. TypeDescriptor does not require your class to be public, but it does require the constructor to be public. I’ve made the class internal because “there are no user-serviceable parts inside”. It just isn’t interesting for anyone other than TypeDescriptor to get at this class, and TypeDescriptor always gets at it through its public base class.
  2. In my constructor, I invoked the base class’s constructor and I passed in the current type description provider for AbstractForm. In this way my custom provider behaves no differently than the default provider except for the places where I override methods.

I’ve now setup a way that I can hook into the metadata that is offered by AbstractForm. One thing to keep in mind is that I am actually hooked into providing metadata for AbstractForm as well as all classes that derive from it. I have to be careful here. A simple mistake could make all derived classes look just like AbstractForm, which isn’t what I want!

There are several virtual methods on TypeDescriptionProvider, but I’m only concerned with one thing: I want ConcreteForm to replace AbstractForm. To do this there are two methods I need to override: GetReflectionType and CreateInstance. Let’s cover each of these in turn.

When the form designer needs to query type information, such as type.IsAbstract, for example, it always uses TypeDescriptor.GetReflectionType to obtain the type it should be reflecting against. This method finds the TypeDescriptionProvider for the type and calls, not surprisingly, GetReflectionType on the provider. The default provider simply returns the type passed into it. I want my provider to return ConcreteForm so that the designer thinks it is designing a concrete class:

public override Type GetReflectionType(Type objectType, object instance) {
    if (objectType == typeof(AbstractForm)) {
        return typeof(ConcreteForm);
    }
    return base.GetReflectionType(objectType, instance);
}

With this code the designer no longer thinks forms derived from AbstractForm are abstract classes. Instead, it sees ConcreteForm, which is not abstract.

That’s half the story. As soon as the designer determines that the base class isn’t abstract it will try to create an instance of it. That will lead to problems – it will try to create an instance of AbstractForm, fail, and put me right back where I started. That’s where the second override comes in. I need to override CreateInstance so I create an instance of ConcreteForm, not AbstractForm:

public override object CreateInstance(
    IServiceProvider provider,
    Type objectType,
    Type[] argTypes,
    object[] args) {

    if (objectType == typeof(AbstractForm)) {
        objectType = typeof(ConcreteForm);
    }

    return base.CreateInstance(provider, objectType, argTypes, args);
}

Now I’m ready to try it out. After building the project I invoked the New Inherited Form dialog. What’s this? I don’t see AbstractForm as an option in the dialog. It turns out that the inheritance dialog doesn’t use GetReflectionType – it should, and I’ll follow up on that. Don’t despair, though, we just need to fiddle with the code a little bit. Instead, I created a standard Form, opened the code, and changed the form to derive from AbstractForm. When I opened the designer…it actually opened. What’s more, I can even change the value of AbstractProperty in the property window. Of course, as my new form is actually deriving from AbstractForm I do get compile errors unless I implement AbstractProperty on my new form class. That’s actually great news – it is why I wanted an abstract class in the first place.

Generalizing ConcreteClassProvider

This mechanism works, but it only works for a single class. What happens if I want to support lots of abstract classes? I wouldn’t want to write a ConcreteClassProvider for each and every class, would I? Instead, I want to generalize ConcreteClassProvider so it can be used to provide a concrete class for any abstract class.

The challenge lies in getting our general-purpose ConcreteClassProvider to know what concrete class should be used for a given abstract class. To do this, I’ve defined a new attribute called ConcreteClassAttribute:

[AttributeUsage(AttributeTargets.Class)]
internal class ConcreteClassAttribute : Attribute {
    Type _concreteType;
    public ConcreteClassAttribute(Type concreteType) {
        _concreteType = concreteType;
    }

    public Type ConcreteType { get { return _concreteType; } }
}

Then, I changed ConcreteClassProvider to look for this attribute on a type when it needs to locate the concrete class. To use it, I need to declare both my custom provider and my new attribute on a class:

[TypeDescriptionProvider(typeof(GeneralConcreteClassProvider))]
[ConcreteClass(typeof(GeneralConcreteForm))]
abstract partial class GeneralAbstractForm : Form {
    public GeneralAbstractForm() {
        InitializeComponent();
    }

    public abstract string AbstractProperty { get; set; }
}

I now have a mechanism I can use for a large scale project.

Summary

While it is a bit of a drag that the Windows Forms designer doesn’t support abstract classes, by using this simple technique you can trick it into doing so. This requires at least Beta 1 of Visual Studio Whidbey. As I found when trying to use the inheritance picking dialog, there may still be pitfalls here, so before going hog wild with your own projects you should make sure this technique works for you in all the scenarios you care about. Microsoft doesn’t officially support designing abstract base classes, so I doubt they’d be very motivated to hotfix any problems you find.

About these ads

Responses

  1. Thank you, thank you, thank you!!! I really needed this functionality and I was at a loss to figure out how to do it!

    If you ever look at this thing anymore (or anybody else wants to answer in the comments), I didn’t really understand how to do the “Generalizing ConcreteClassProvider” section. I only had 2 abstract controls, though, so I just did everything twice. Could you explain specifically how you “changed ConcreteClassProvider to look for this attribute on a type when it needs to locate the concrete class.” And in the final bit of code you have GeneralConcreteClassProvider and GeneralConcreteForm, but those aren’t mentioned anywhere above. Did you just rename the previous classes to those names?

    • @Shanna : should I implement this in the base form? I am unclear where the code should reside.

  2. Excellent, this is just what I was lookng for. Thank you. I’ve noticed a lot of people looking for a way around the VS designer issue. This solves it nicely.

  3. Hey

    I wonder if it works if I have one class
    abstract public partial class MyOwnClass: UserControl
    and many class which derived by MyOwnClass ?

    Without ‘Generalizing ConcreteClassProvider’ approach I can show only one class which derive from MyOwnClass, Is ConcreteClassProvider approach allow me to do that?

    Every class derive from MyOwnClass has the same look.

    Thanks for posting Yours article.

    Best Regards
    Przemek

  4. You should correct your example:
    GetReflectionType should only replace ‘objectType’ and call base.GetReflectionType instead of return typeof(ConcreteForm):

    public override Type GetReflectionType(Type objectType, object instance) {
    if (objectType == typeof(AbstractForm)) {
    objectType = typeof(ConcreteForm);
    }
    return base.GetReflectionType(objectType, instance);
    }

    otherwise you coludn’t resize form in designer in some cases.

  5. Thanks for the article, this helped me a lot!
    I generalized it by using:
    while (objectType.IsAbstract)
    objectType = objectType.BaseType;

    This works as well. At least in my situation.

  6. It only works one time. Each time I modify my form, I have to close visualStudio and restart it :(

    TypeDescriptionProvider seem to be execute at startup and it will not be refresh until a restart visualstudio.

    I put a MessageBos.show(“Hello”); in my TypeDescriptionProvider class. It trig the messageBox. Yeah but then I comment the line and the message still trig. Which confirm me that TypeDescriptionProvider is loaded at startup.

    Does anyone have the same issue ?
    Is there I way to force VisualStudio to refresh ?

  7. Can anyone provide a sample project of this working and post it up somewhere as a zip?


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: