The Perfect Host
Create And Host Custom Designers With The .NET Framework 2.0
Dinesh Chandnani
This article discusses:
- Understanding designers and services
- Creating a hosting form
- Building and using a toolbox
- Loading and managing designers
|
This article uses the following technologies: Windows Forms, Visual Studio 2005 |
Code download available at: DesignerHosting.exe (229 KB)
Browse the Code Online
Contents
Extensibility with Services
DesignSurface and DesignSurfaceManager
The Hosting Form
The Toolbox
Multiple DesignSurfaces
DesignerLoaders
CodeDomDesignerLoader
Conclusion
Version 1.0 of the Microsoft® .NET Framework provided a very flexible design-time architecture, but offered virtually no implemented code to actually create and host designers. All of the hosting logic was implemented in Visual Studio® .NET, requiring third parties to rewrite all of this complex logic. This has now changed; the .NET Framework 2.0 introduces a set of classes that can be used to host designers right out of the box.
Figure 1 Runtime
To understand how .NET Framework designers work, it is important to know how designers are used. A designer is an object that only exists at design time and that connects to an object that normally exists at run time. The framework connects these two objects and provides a conduit for the design-time object to augment the behavior of the run-time object. At run time, a form and a button on that form are connected only through the parent/child relationship between the two controls (see Figure 1). There is no other object that controls the lifetime of these controls.
Figure 2
The picture looks a little more complex at design time. Both the form and the button have designers associated with them. Both objects are also connected to a host container (see Figure 2), which owns these two objects. The host container also provides services—such as selection services to select one or more objects at design time, and UI services for displaying messages, invoking help, and interacting with the development environment—that the objects and designers may use.
The host container has many responsibilities. It creates components, binding them to designers and providing services to the components and designers it maintains. It loads designers from some sort of persistent state and saves them back to that state. The host container provides logic for undo, clipboard functions, and many other services that the designers rely upon to deliver a robust design-time environment.
Figure 3 Designer Hosting
The host container can also persist the state of the designer. To do this, it would use a designer loader. The designer loader may, in turn, use serializers to serialize the components, as shown in Figure 3.
Extensibility with Services
The .NET Framework designer architecture is extensible. Critical to this extensibility, services provide the ability to enhance the functionality available to the various designers. A service is any object that can be queried by a type. Typically you would define some abstract class or interface that represents the service and then provide an implementation of that service. You can add services to and remove services from an object called a service container. IDesignerHost, the primary host interface for designers, is a service container. A service is a feature that may be shared between components written by different parties. Because of this, you must follow certain rules when using and creating services.
Services are not guaranteed. Whenever you ask for a service by calling the GetService method, you must always check to see whether GetService returned a valid object. Not all services are available on all platforms, and services that were once available may not be available in the future. Thus, your code should be written to degrade gracefully, usually by disabling the feature that required the service, in case a service does become unavailable.
If you add a service, remember to remove it when your designer is disposed. The designer host may destroy and recreate your designer from time to time. If you fail to remove a service, the old designer may be left in memory.
DesignSurface and DesignSurfaceManager
The .NET Framework 2.0 introduces two classes that are used for hosting designers and providing services to the designers: DesignSurface and DesignSurfaceManager. DesignSurface is what the user perceives as a designer; it is the UI the user manipulates to change design-time features. DesignSurface may be used as a standalone designer or it may be coupled with DesignSurfaceManager to provide a common implementation for an application that hosts multiple DesignSurfaces.
DesignSurface provides several design-time services automatically (see Figure 4). Most of these can be overridden in the service container. It is illegal to replace the non-replaceable services because their implementations all depend on each other. Note that all services added to the service container that implement IDisposable will be disposed when the design surface is disposed.
Figure 4 Default DesignSurface Design-Time Services
Service |
Replaceable |
Description |
IComponentChangeService |
No |
Raises events as changes are made to components. |
IDesignerHost |
No |
Master interface for designers. Controls access to types, services, and transactions. |
IContainer |
No |
Each designer has an IContainer that owns the set of components that are being designed. |
IServiceContainer |
No |
Derives from IServiceProvider and provides a way to add and remove services from the designer. |
IExtenderProviderService |
Yes |
Allows objects that are not part of the container's Components collection to provide their own extender providers. The IExtenderProviderService allows clients to register IExtenderProviders. An IExtenderProvider provides new custom properties on existing objects such as Modifiers. |
IExtenderListService |
Yes |
Used by TypeDescriptor to get a list of extender providers. This allows extender providers to live outside of the container. |
ITypeDescriptorFilterService |
Yes |
Primary interface for metadata filtering. This provides designer metadata hooks. You can use this, for example, to hide properties from the Property window that should not be displayed or to change other attributes on components. |
ISelectionService |
Yes |
Provides a way to select components in the designer. |
IReferenceService |
Yes |
Provides a way to get a name for objects, even when those objects are not sited. |
IPropertyValueUIService |
Yes |
Provides a simple property/UI dictionary that can be used by property windows and designers to add glyphs to properties. |
In addition to the default services, DesignSurface also provides IDictionaryService, available through a component's site. This service, which provides a generic dictionary of key/value pairs that can be used to store arbitrary data about a component, is unique to each component. It is not possible to replace these services because there is no way to replace services on a per-site basis.
DesignSurfaceManager is intended to be a container of designers. It provides common services that handle event routing between designers, property windows, and other global objects. Using DesignSurfaceManager is optional, but recommended if you intend to have several designer windows.
DesignSurfaceManager also provides several design-time services automatically (see Figure 5). Each of these can be overridden by replacing their value in the protected ServiceContainer property. As with DesignSurface, all DesignSurfaceManager services added to the service container that implement IDisposable will be disposed when the designer application is disposed.
Figure 5 DesignSurfaceManager Design-Time Services
Service |
Description |
IUIService |
Provides a way for components to show a UI such as error messages and dialog boxes |
IDesignerEventService |
Provides a global eventing mechanism for designer events |
IDesignerEventService is a particularly useful service. It allows an application to know when a designer becomes active. IDesignerEventService provides a collection of designers and is a single place where global objects, such as the Property window, can listen to selection change events.
The Hosting Form
To demonstrate how simple it is to host a designer, I wrote the following sample code to create a basic Windows® Forms designer and display it:
Copy Code
// Create the DesignSurface and load it with a form
DesignSurface ds = new DesignSurface();
ds.BeginLoad(typeof(Form));
// Get the View of the DesignSurface, host it in a form, and show it
Control c = ds.View as Control;
Form f = new Form();
c.Parent = f;
c.Dock = DockStyle.Fill;
f.Show();
In this code snippet, I have loaded the DesignSurface with a Form. Similarly, you can load the DesignSurface with any component that has a root designer available for it. For example, you could load a UserControl or a Component instead.
The sample provided in the code download for this article hosts four different root components: Form, UserControl, Component, and MyTopLevelComponent (a graph designer). When you run the sample, a shell UI will open. This interface includes a toolbox, a properties browser, a tab control for hosting designers, an output window, and a Solution Explorer, as shown in Figure 6. Selecting File | New | Form from the menu opens a new designer host with Windows Forms Designer. This essentially uses the code I've just shown you to load the designer. Rather than loading a Form, the sample app illustrates how to load a UserControl or Component.
Figure 6 Hosting Windows Forms Designer
To create a root component, create a designer that implements IRootDesigner and then associate this designer with your component. The View property of the root component specifies the view that will be presented to the user.
One of the main services provided by DesignSurface is IDesignerHost. It is the master interface used to provide designers and controls access to types, services, and transactions. It can also be used to create and destroy components. To add a button to the Windows Forms designer I created earlier, all I need to do is get the IDesignerHost service from the DesignSurface and use it to create the button as shown in Figure 7.
Figure 7 Create a Button with IDesignerHost
Copy Code
// Add a Button to the Form
IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost));
Button b = (Button)idh.CreateComponent(typeof(Button));
// Set the Parent of this Button to the RootComponent (the Form)
b.Parent = (Form)idh.RootComponent;
// Use ComponentChangeService to announce changing of the
// Form's Controls collection */
IComponentChangeService icc = (IComponentChangeService)
idh.GetService(typeof(IComponentChangeService));
icc.OnComponentChanging(idh.RootComponent,
TypeDescriptor.GetProperties(idh.RootComponent)["Controls");
IToolboxUser specifies that the designer supports adding controls from the toolbox. This means that if you do have a toolbox that implements ToolboxService, you can use the IToolboxUser interface to add controls to the root component. For instance:
Copy Code
/* Add a Button to the Form using IToolboxUser */
IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost));
IToolboxUser itu = (IToolboxUser)idh.GetDesigner(idh.RootComponent);
itu.ToolPicked(new ToolboxItem(typeof(Button)));
When controls are added to the custom RootDesigner in the sample application by double-clicking the items in the toolbox, the view of the RootDesigner updates to display a pie chart as shown in Figure 8. Clicking on the GraphStyle link changes the view to a bar graph.
Figure 8 Custom RootDesigner Updates
The Toolbox
MyRootDesigner implements the IToolboxUser interface. This has two methods: GetToolSupported and ToolPicked. You can use GetToolSupported to filter the items that can be added onto the designer. ToolPicked eventually calls into the CreateComponents method (which, as the name implies, is responsible for creating the components) of ToolboxItem.
Now that I have added the controls and components to the designer, let's take a closer look at how to implement a toolbox. First, your toolbox needs to implement IToolboxService—this service is added to the service container and can be accessed by anyone who needs to use it. The main functions of IToolboxService are shown in Figure 9.
Figure 9 IToolboxService Members
Method or Property |
Description |
GetSelectedToolboxItem |
Returns the selected toolbox item |
SerializeToolboxItem |
Creates a binary serialized object (DataObject) from ToolboxItem |
DeserializeToolboxItem |
Returns the ToolboxItem from a binary serialized object (DataObject) |
To allow the items from the toolbox to be added onto the designer using the mouse or keyboard, the toolbox in the sample hooks onto the KeyDown and MouseDown events. For the Enter key or a mouse double-click, IToolboxUser.ToolPicked is called. The sample shows how to serialize the ToolboxItem into DataObject and DoDragDrop for when a mouse single-click event occurs. IToolboxService.SerializeToolboxItem will be called on mouse up and the item will be added to the designer.
When a new control or component is added to the designer, you can provide a custom name for the control by implementing INameCreationService. The sample application shows an example of this service in action using CreateName, ValidateName, and IsValidName.
Multiple DesignSurfaces
When managing multiple DesignSurfaces, it is a good idea to use a DesignSurfaceManager. This makes it easier to manage these DesignSurfaces. (Note that the services of DesignSurfaceManager are also available to DesignSurface.)
Calling DesignSurfaceManager.CreateDesignSurface calls into CreateDesignSurfaceCore. You can override this function to create a custom DesignSurface and add the services. The sample app creates a custom HostSurface by overriding this function in the HostSurfaceManager class:
Copy Code
protected override DesignSurface CreateDesignSurfaceCore(
IServiceProvider parentProvider)
{
return new HostSurface(parentProvider);
}
You can then hook into the ActiveDesignSurfaceChanged event and update the output window in the HostSurfaceManager class, as shown here:
Copy Code
void HostSurfaceManager_ActiveDesignSurfaceChanged(
object sender, ActiveDesignSurfaceChangedEventArgs e)
{
ToolWindows.OutputWindow o =
this.GetService(typeof(ToolWindows.OutputWindow)) as
ToolWindows.OutputWindow;
o.RichTextBox.Text += "New host added.\n";
}
DesignerLoaders
So far I've created DesignSurfaces, hosted designers, added controls, implemented a toolbox, and added and accessed services like OutputWindow. The next step is to persist the designer. The designer loader is, as you'd expect, responsible for loading the designer from some persistent state. Simple and flexible, designer loaders have very few requirements. In fact, you can create an instance of the Windows Forms designer with a one-line designer loader that simply creates an instance of System.Windows.Forms.Form.
In addition to loading a form design, a designer loader is also responsible for saving the design. Because saving is an optional behavior, a designer loader listens to change events from the designer host and automatically saves state according to these events.
The .NET Framework 2.0 introduces two new classes for writing custom loaders: BasicDesignerLoader and CodeDomDesignerLoader. The sample app illustrates implementations of both loader types. Earlier I demonstrated loading the DesignSurface with a root component by passing in the type of the component. However, if you are using a loader, it should be used to load the design surface. The BeginLoad code snippet you'll need when using loaders should look something like this:
Copy Code
// Load it using a Loader
ds.BeginLoad(new MyLoader());
The DesignerLoader is responsible for loading the root component in the DesignSurface and creating any components. When creating a new form or any other root component, the loader simply loads it. In comparison, when loading from a code file or some other store, the loader is responsible for parsing the file or store and recreating the root component along with any other necessary components.
The .NET Framework defines an abstract base class called DesignerLoader that is used to load and save designers from persistent storage. The base class is abstract so that any type of persistence model can be used. This, however, adds to the complexity of implementing the class.
BasicDesignerLoader provides a complete and common implementation of a designer loader minus any information related to the persistence format. Like DesignerLoader, it is abstract, not dictating anything about the persistence format. BasicDesignerLoader does, however, handle the standard work of knowing when to save, knowing how to reload, and tracking change notifications from designers. Its features include support for multiple load dependencies, tracking of the modified bit to indicate a need to save changes, and deferred idle time reload support.
The services shown in Figure 10 are added to the designer host's service container by BasicDesignerLoader. As with other services, you can change replaceable services by editing their value in the protected LoaderHost property.The sample app implements a BasicDesignerLoader that persists the state in an XML format. To see how it works, select File | Type | BasicDesignerLoader. Then create a new Form by selecting File | New | Form. To see the XML code that is generated, select View | Code | XML. The XML code that is generated by the app will look something like this:
Copy Code
<Object type="System.Windows.Forms.Form, System.Windows.Forms,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="Form1" children="Controls">
<Property name="Name">Form1</Property>
<Property name="DataBindings">
<Property name="DefaultDataSourceUpdateMode">OnValidation</Property>
</Property>
<Property name="ClientSize">292, 273</Property>
</Object>
PerformFlush and PerformLoad are the two abstract functions of the BasicDesignerLoader that you need to implement for serializing and deserializing, respectively.
Figure 10 BasicDesignerLoader Services
Service |
Replaceable |
Description |
IDesignerLoaderService |
Yes |
Allows objects to request that the designer reload itself at idle. |
IDesignerSerializationManager |
No |
Used to serialize and deserialize objects. The serialization manager is added as a service so anyone who needs to perform serialization can utilize any serialization providers that were added to the serialization manager. |
CodeDomDesignerLoader
Design-time serialization is handled by generating source code. One of the challenges of any code-generation scheme is the handling of multiple languages. The .NET Framework is designed to work with a variety of languages, so I also want the designers to emit to several languages. There are two ways to address this. The first is to require each language vendor to write the code-generation engine for their language. Unfortunately, no language vendor can anticipate the wide variety of code-generation requirements that third-party component vendors may need. The second approach is to require each component vendor to provide code generation for each language they want to support. This is equally bad, because the number of languages supported is not fixed.
To solve this problem, the .NET Framework defines an object model called the Code Document Object Model (CodeDOM). All source code can essentially be broken down into primitive elements and the CodeDOM is an object model for those elements. When code adheres to CodeDOM, the generated object model can later be sent to a code generator for a particular language to render the appropriate code.
The .NET Framework 2.0 introduces CodeDomDesignerLoader, which inherits from BasicDesignerLoader. The CodeDomDesignerLoader is a full loader that works by reading and writing CodeDOM. It is a turnkey designer loader, so all you need to do is provide the CodeDOM.
In the sample app you can select File | Type | CodeDomDesigner-Loader to see an example of the CodeDOM in action. Create a new form by selecting File | New | Form—this creates a DesignSurface and loads it using a CodeDomDesignerLoader. To check the code, select View | Code | C# to see the C# version of the form's code, or View | Code | VB to see the Visual Basic® version.
To generate the code, the sample app uses CSharpCodeProvider and VBCodeProvider. It also uses the code providers to compile the code and run the executable (see Figure 11).
Figure 11 Generate, Compile, and Run
Copy Code
CompilerParameters cp = new CompilerParameters();
AssemblyName[] assemblyNames =
Assembly.GetExecutingAssembly().GetReferencedAssemblies();
foreach (AssemblyName an in assemblyNames)
{
Assembly assembly = Assembly.Load(an);
cp.ReferencedAssemblies.Add(assembly.Location);
}
cp.GenerateExecutable = true;
cp.OutputAssembly = executable;
cp.MainClass = "DesignerHostSample." +
this.LoaderHost.RootComponent.Site.Name;
// Compile CodeCompileUnit using CodeProvider
CSharpCodeProvider cc = new CSharpCodeProvider();
CompilerResults cr = cc.CompileAssemblyFromDom(cp, codeCompileUnit);
if (cr.Errors.HasErrors)
{
string errors = string.Empty;
foreach (CompilerError error in cr.Errors)
{
errors += error.ErrorText + "\n";
}
MessageBox.Show(errors, "Errors during compile.");
}
ITypeResolutionService, which is required when using the CodeDomDesignerLoader, is responsible for resolving a type. One scenario, for example, where this service is called to resolve types is when adding a control from the toolbox into the designer. The sample application resolves all types from the System.Windows.Forms assembly so that you can add controls from the Windows Forms tab of the toolbox.
Conclusion
As you've seen, the .NET Framework provides a powerful and flexible designer hosting infrastructure. Designers provide straightforward extensibility that help to address very specific needs or more advanced scenarios than were supported by previous versions of Visual Studio. Of course, designers can be hosted easily outside Visual Studio, as well. Be sure to download the sample application, so you can play around with the code and start implementing your own custom designers today.
Dinesh Chandnani is a Software Design Engineer in Test for the .NET Client Team at Microsoft, working on designer hosting and other designer features. He started working for Microsoft in 2002 after completing his masters' degree in Computer Science at the University of Arizona.