Hosting Windows Forms Designers, Part 1

divil

Ultimate Contributor
Joined
Nov 17, 2002
Messages
2,746
Location
England
Because of size limitations I have had to strip the embedded code samples from this article and split it in two. However, you can still View the full article on my website.

Introduction

I decided to write this article not because there is a strong demand for this information, but because there is literally no existing information out there on the topic. The documentation is scarce if any, and aside from a few tidbits thrown out by Microsoft it is a daunting task. It requires you to already be very familiar with the design-time architecture, and have a strong grasp of all the interfaces you commonly use.
All the interfaces you rely on when developing designers and extending the design-time behaviour of your controls, it is now your responsibility to implement. Not only is this a non-trivial task, but it is one of those times where you have to implement a great deal before you can see any progress at all. That said, once you've written the code you'll never have to write it again (and since I'm going to be writing it for this article, you'll never have to write it in the first place).
Why would you want to host the Windows Forms Designers? Well, there could be lots of reasons. When I was faced with the problem it was because I was writing an IDE for .NET languages and I wanted a visual interface to configure GUIs, just like VS.NET has. When you consider the flexibility of the designer architecture, you realise you have a framework for designing any 2-dimensional hierarchical system. All the code for moving, resizing, creating, deleting and configuring items is already there.
The one you see in VS.NET is running from the framework, and is the same one we'll be writing the code to enable. All the designer stuff is already present in the framework, you just need to write the code to bind it all together. This was best left out of the framework because it is strongly tied to the host environment.

The Service Hierarchy

The most fundamental thing to be familiar with on this topic is the service hierarchy. You have already used it, whenever you make a call to GetService to obtain an instance of ISelectionService or IComponentChangeService. A service hierarchy is made up of several ServiceContainers. A ServiceContainer is used to store an instance of a class that provides a service against its type. So you request a instance by passing it a type, and it comes back with what you need.
When a ServiceContainer is created it is usually bound to a parent. This means that when a service is requested and it isn't in one container, it asks its parent, and then that asks its parent, and so on. Using the example of the Visual Studio IDE, services are instantiated at several levels. At the document view level you have services such as ISelectionService and IComponentChangedService. These are obviously only relevant to one document (and only the design view, not the code view).
Look at the ITypeResolutionService interface though. Chances are you'll never use it, it's how the designers discover and instantiate types as they deserialize a document. Although the view-level servicecontainer will get the request for it, it will be the servicecontainer at the project level that will handle the request. Visual Studio is made up of a hierarchy of servicecontainers, from the toplevel ones that are used internally to manage menus and colours, to the document and view-level ones for editing documents.
It's very important when hosting designers to get the servicecontainer hierarchy right - it can make things more logical and therefore more understandable. Always remember that the way everything at design time works together is by getting and using services from the host environment.

The Design Container

No matter what you're designing, be it Windows Forms or Web Forms, everything in design mode is said to be on the Design Surface. What this means is that it's in the design container. Forms and controls are hierarchical in nature, but everything is also in the design container, which is a one-level collection. This container has the code to discover and instantiate designers for objects, and also dispose of them when they're done.
As you probably already know, all designers implement the IDesigner interface. However, there is always one special designer in a document which has to implement IRootDesigner. This is the one that acts as a parent to all the rest, and is responsible for providing the interface you use to view the object you're designing. This is always the first object to be added to the design container. The framework provides three root designers - for Forms, UserControls and Components. You've probably seen all these if you've used the various designable types in Visual Studio. The framework doesn't contain any Web Forms root designers.
The root designer is a great idea, it means you don't have to write any special hosting code no matter what you're designing. Once your hosting framework is in place you can design anything that offers one.

The Designer Host

This is where you start when hosting designers (obviously). It all begins with the IDesignerHost interface, which you might well have already used from designers you've written. It exposes the design container, the root component, and methods for creating components and getting their designers. It also provides the support for designer transactions and acts as a service container.
Although we'll be implementing quite a few interfaces apart from this one, the most important two are ISelectionService and IComponentChangeService, because every designer uses them. Thankfully most of the .NET controls behave gracefully when they fail to find a service implemented, if they didn't we would have a much harder time getting started.
The core designer hosting architecture is split in to several interfaces, but since they are tightly bound together we will be implementing the most important ones all in one class:

IDesignerHost - The core interface.
IContainer - The container that holds all components on the design surface.
IComponentChangeService - Used to broadcast events when components change.
IExtenderProvider - Used to give components a configurable name.

Design Sites

In order to be on the design surface, a class has to implement IComponent. This means it has a Site property, which is how its name is kept track of at design time. For every object that is placed on the design surface an object implementing ISite needs to be assigned to it. It's through this that the components are able to request services, establish that they are in design mode and get their name.
We will create a class, DesignSite, that implements ISite and provides this necessary information. Anyway, enough with the theory, let's get down to business.

Starting Off

First we'll create our class, DesignerHost, and make sure we have a reference to System.Design.dll. A lot of the interfaces we'll be using are in there. We will accept an instance of something implementing IServiceContainer as the parameter to the constructor, and since we have to provide an IServiceContainer implementation ourselves we'll simply wrap that one. Since that's the easiest part we'll do it first.
At the same time as implementing IDesignerHost we will implement IContainer. This is the design container I spoke of before. All the code we're writing really revolves around the code that gets and instantiates designers for the objects added to the host.

IContainer.Add

This method has two overloads, one where a name is passed and one without. We'll just defer the latter to the former, passing null and dealing with it correctly. This is one of the methods with the real meat in.
The first thing we do after checking the passed component isn't null, is to check the component isn't already sited somewhere. If it is, we remove it from its current container. Then we generate a name for the component if we haven't been passed one, using INameCreationService. We'll come on to implementing this interface later. Next we make sure there isn't already a component with the passed name in the container.
The next stage is to give the new component a Site, which we do with our ISite implementation. I'll come on to that later. Giving it a site as early as possible is important, because that's how the component is able to request services from its environment. Then we make the important call to TypeDescriptor.CreateDesigner, which actually finds and created the designer associated with this component. If this is the first component being added we look for an IRootDesigner and set the rootComponent field.
Next comes a check to make sure we got a designer, and if so we initialize it. Remember the Initialize method of a designer that you override when making your own? This is where we actually call that method. After this we see if this component is an extender provider, and if so add it to the list we maintain. Yes, we have to write the functionality to keep track of those. All the magic is taken out of the designer architecture when you have to implement it yourself. Finally we add the component to our internal container instance.

IContainer.Remove

Although not quite as complicated as IContainer.Add, this method is responsible for the cleanup process of removing a component from the design surface.
The first couple of check we do are to make sure the component passed isn't null, and to make sure the component actually belongs to our design surface. Then we wrap the rest of the method in ComponentRemoving and then ComponentRemoved calls so that classes listening to our IComponentChangeService implementation know what's going on.

Part 2 ->
 

Attachments

Back
Top