It's time for my second post on Cider's design time architecture. Last time I introduced the editing context and described how services and context items work. It's now time to talk about how we're going to do extensibility in Cider. Before I can do that I need to explain what I mean when I talk about an extensible design time architecture. Then, I need to give you a little background on how Windows Forms implements their extensibility, and finally I can start to describe how we do it in Cider and why we chose to do things differently.
Design Time Extensibility
In the good old days of ActiveX controls, designers weren't very extensible. The property window for VB 6, for example, was hard-wired through the magic of OLE Automation to know about exactly four data types: fonts, colors, enums and variants. The VB 6 designer treated all controls as opaque rectangles. As long as your custom control could fit into these four data types and be treated as a rectangle that could be placed and sized in X-Y coordinates, you were happy. The introduction of Windows Forms required that we re-think some of this. For example, the .NET Framework introduced many new data types. Having a property window that only understood a limited set of data types would have made learning the increased depth of Windows Forms much more difficult. Also, Windows Forms introduced concepts like pluggable control layouts, so assuming all controls fit a standard rectangular X-Y model would have been a big compromise.
To help with this, when we designed Windows Forms we also designed an extensibility model that described how Windows Forms controls behaved at design time. We wanted to separate this design time code from the normal runtime Windows Forms classes so it didn't have to be loaded at runtime. We also had a simple rule: if we wanted to do it, it's a pretty good bet that some other company will want to do it too. That meant that our designer had to be built from the ground up using the same extensibility model that we exposed to 3rd parties. I still think that's a very important philosophy.
Windows Forms got its extensibility from a class called a designer. Designers had their own object model and class hierarchy, and you related a designer to a control through a custom attribute:
[Designer(typeof(MyControlDesigner))] public class MyControl : Control {} internal class MyControlDesigner : ControlDesigner { }
Infrastructure code sniffed for the presence of a designer attribute and instantiated the designer contained within it. The real magic around how the form designer for Windows Forms works is contained in the various classes that derive from ControlDesigner.
The Windows Forms ControlDesigner Class
ControlDesigner is the base class that Windows Forms uses to introduce design time functionality. There are methods on this class that allow you to be notified when the user clicks and drags on your control. By themselves, these methods don't give you much. The power of the Windows Forms designer model comes from all the services that are made available from ControlDesigner's GetService method. For example, to display a message to the user, you could use the IUIService:
IUIService uis = GetService(typeof(IUIService)) as IUIService; if (uis != null) { uis.ShowMessage("Hello World"); } else { MessageBox.Show("Hello World", "Message", MessageBoxButtons.OK; }
If I use IUIService here, I get a message box that has its title bar, icon and buttons setup by the host application. Note that I also handle the case where IUIService doesn't exist. Services in the Windows Forms designer architecture are optional and you must provide a fallback if a service doesn't exist. This hints at one of the hardest parts of programming to this model. That's a topic for the next section.
Observations
We learned a lot from the architecture we designed for Windows Forms. The most important thing we learned was that people loved having the ability to add rich design time features for their controls. Adding rich designability is a lot of work -- sometimes almost as much work as writing the control to begin with -- yet still developers worked hard to add these types of features because it made their controls easier for their customers to use. We even found that developers of typical line-of-business applications often added design time features because it increased the productivity of their team.
We also learned that there was a nontrivial amount of pain developers had to go through when writing design time logic. For one, the service model makes the learning curve very steep. The GetService method is essentially a black box so there is no Intellisense to guide you. Also, notice above that I provided a fallback if a service didn't exist. This is important because the presence of any service is entirely optional. The most common fallback is to check for null and then do nothing, but that makes finding bugs very difficult. Worse, in many cases people (including us!) forgot to check for null, resulting in crashing designers in certain scenarios.
In addition to the service complexities, we came to the realization that designers really need to follow the same public class hierarchy as the controls they design. Suppose we have a base class named Control with a corresponding designer named ControlDesigner. We also have Button, which derives from Control. Button doesn't have any special needs at design time so we did not create ButtonDesigner.
Now imagine that a third party develops a new FunkyButton class that derives from Button. FunkyButton adds all sorts of cool design time functionality through its FunkyButtonDesigner. FunkyButtonDesigner derives from ControlDesigner, because there is no ButtonDesigner. What happens when we release a new version of the .NET Framework? What if we beef up the design time features of Button by adding a ButtonDesigner class? Our Button class works fine, but what about FunkyButton? Its designer doesn't derive from ButtonDesigner because it didn't exist. At best, FunkyButton simply doesn't have the new hottness provided by the new ButtonDesigner. At worst, FunkyButton is now broken in the designer.
Our goal with Cider's extensibility architecture was to improve these experiences.
Cider's Extensibility
We wanted Cider's model to be easier to use without sacrificing the flexibility we obtained from the service model. We split our developer base into two categories:
- People who want to plug into existing design time features.
- People who want to write entire new design time features.
Understanding this bifurcation is easier if I provide an example. Let's say that Cider has a feature that allows you to provide a "persistent adorner" (it does). This is a glyph or other graphic that is always displayed for a control. If you put a grid, canvas or other panel on a form today in Cider, you will see a faint dotted outline that defines the edge of the panel. This outline is a persistent adorner and serves to show where the edge of these transparent objects reside.
There are two parts to this feature: a bunch of glue and maintenance logic that finds a control's persistent adorner and renders it, and all the controls in the world that offer their own persistent adorners. If I'm writing some new control and I want it to have a persistent adorner, I shouldn't have to be aware of all the plumbing logic. As a control vendor, I fall into the first category above. I don't care about the plumbing needed to make a feature work. I just want to write the bare minimum needed to make the feature work for my control.
Cider's extensibility model is split into two parts. As a feature implementer I define the extensibility model I want control vendors to use by defining a class that derives from Extension. Extension is the base class for all extensibility in Cider. Extension has a very simple API:
public abstract class Extension { }
Not much too it, is there? That's on purpose. Let's look at what it takes to define the persistent adorner extension:
public class PersistentAdornerExtension : Extension { public Visual Adorner { get; set; } }
To use this extension, a control vendor derives from it and adorns their control with an attribute:
class MyPersistentExtension : PersistentAdornerExtension { public MyPersistentExtension() { Adorner = new Rectangle(); } } [Extension(typeof(MyPersistentExtension))] public class MyControl : Control {}
The plumbing logic I wrote as a feature implementer will find this extension, grab the rectangle in it, and display it appropriately hovering over the control. Obviously, there is more work on the feature implementer's side, but that's on purpose. Next I'll show you how to implement this feature.
Implementing Features
The code I showed above is only a small part of the persistent adorner feature. In the Windows Forms designer, this feature would have been baked into the core ControlDesigner class. Cider has no such core designer class, however. Cider is simply a loose collection of features built out of extensions. Where is the logic needed to find the adorner and display it? It's in a class called an ExtensionServer:
public abstract class ExtensionServer { protected ExtensionServer(EditingContext context) {} protected EditingContext Context { get; } protected IEnumerable CreateExtensions(object value) {} }
An extension server is typically implemented as a non-public class because it has no public API. Notice that it accepts an editing context in its constructor. Extension servers use the editing context to access services and context items. With access to this data the extension server has the same power that designers had in the Windows Forms world. The advantage is that it is only the extension server that needs to be coded to deal with services.
Notice that in my example above I only declared that MyPersistentExtension should be associated with MyControl. How does the extension server become part of the action? Whoever implements the PersistentAdorner feature links the extension type to the extension server type with an attribute:
[ExtensionServer(typeof(MyPersistentExtensionServer))] class MyPersistentExtension : PersistentAdornerExtension { }
When the designers sees an instance of the MyControl type for the first time, it walks its attributes looking for extensions. For each extension it finds, it looks up the corresponding extension server. If it has not yet created an instance of the extension server, it creates one. One big advantage of this technique is how well it scales. Instead of one designer for each control, you have one extension server for each feature of the designer, but that one extension server can handle many controls. The extensions themselves are often transient. The PersistentAdornerExtension is a great example of this: once the extension is created and the adorner taken from it, the extension itself is garbage collected.
One last thing remains: while this technique allows the vast majority of developers to stay far away from service providers, those developers who write new features for the designer are immersed in services. So far, we've done nothing to help the pattern of always checking for the presence of a service before using it. I'll cover that in the subscriptions section next.
Subscriptions
Extension servers support a feature called a subscription. An extension server can declare what services and editing context items it uses by declaring a subscription to them. Say, for example, that my MyPersistentExtensionServer needs access to some service called AdornerLayerService. I can declare a subscription to that service as follows:
[SubscribeService(typeof(AdornerLayerService))] class MyPersistentExtensionServer : ExtensionServer { }
By placing a SubscribeService attribute on my extension server I've declared that my code cannot run without that service. The designer inspects these attributes before it creates an instance of any extension server. If the set of subscriptions cannot be met, the extension server is put on a "pending" list. It won't be activated until all the services it requires are available. By adding this attribute, I guarantee that by the time my constructor is invoked, the AdornerLayerService is available in the editing context and a call to GetService is guaranteed to succeed. This also eliminates order dependencies between extension servers.
Summary
Well, that's about it for the key parts of Cider's extensibility model. As I wrote this one of the things that came to mind was wow, do some of those class names stink. I'm sure we'll iterate on those a bit before we call them done. Most of what people will be doing with Cider will be at a much higher level than what I've described here, but you've got to learn to walk before you learn to run. In the next installment I'll cover how Cider builds on this extension model to provide user input to the designer.