Quantcast
Channel: Pocket Silicon - Cider
Viewing all articles
Browse latest Browse all 20

Introduction to the Cider Designer's Architecture

$
0
0

This is the first of a series of posts about the architecture of the Cider designer. Cider isn’t going to be shipping for a long time, so this isn’t information you can immediately put to use. Actually, I’m hoping that by publishing our plans very early like this I can get feedback about what you like and what you don’t like. That way I can get the stuff you don’t like out of the product before you see it. I’m going to roll this out by first describing to you how everything works, and then I’ll get into what the “everything” part is.

The first big shocker is that Cider does not use the IComponent / IDesigner architecture that Windows Forms and ASP.NET use. The most obvious reason for this is that nothing in WPF implements IComponent, but we had a lot of good reasons not to use this existing architecture. Here are just a few of them:

  • IComponent requires too much planning ahead. If you wanted something to be designable, you had to implement IComponent. But, as the types of designers in the world grow beyond form and UI designers, all of a sudden everything looks like it could be designable. Cider only requires that you derive from System.Object, which should be pretty easy for most folks.
  • IContainer, which is the counterpart to IComponent that makes all the designers work, assumes that all components being designed can be expressed as a simple linear collection of objects. While it is possible to flatten most any graph to a linear list, in WPF XAML trees can be quite huge, surpassing tens of thousands of elements. It takes some specialized data structures to handle object graphics of that size, and exposing this through a collection interface would impact the performance of everyone who accessed the interface.
  • In a word, services. IServiceProvider, which is the key to providing design time functionality in the IComponent world, is a big black box. In fact, it’s infinitely big: it is impossible to shine a light through GetService to see what’s inside. This makes documentation a key requirement to doing anything worthwhile in the designer. Also, because a service is never guaranteed to exist nor is it guaranteed to continue existing once you find it, programming to GetService in a robust fashion is very hard. Cider still is built on top of services, but only some things in the designer need to be aware of it. Also, Cider has a richer interface that builds on top of IServiceProvider to shine some light into the box.

One of the key requirements for Cider, however, is interop with other technologies like Windows Forms. We will teach Cider enough about IComponents and IDesigners in order for Cider to create designers for IComponent-based classes, so exiting components will work just fine in Cider. Now, let’s dig into the foundation of Cider’s architecture.

The Editing Context

All Cider designers start with a class called an editing context. If you’re familiar with the IComponent world, Cider’s editing context is somewhat related to IDesignerHost. Compared to IDesignerHost, however, Cider’s editing context has been put on an extreme diet. The EditingContext class keeps track of just two pieces of information: a dictionary of services and a dictionary of context items.

EditingContext is a good example of the kinds of design patterns you’ll find in Cider. Cider has very little coupling between its various systems. You can almost think of it as a grab bag of utility classes that, when combined in the right way, produce a working designer. This helps keep Cider nimble over time. Don’t be fooled by the simplicity of this class, however. It offers some very useful functionality. Let’s start by looking at how it exposes services.

Services

Services in Cider are similar to services in the IComponent world. To Cider, a “service” is an instance of a class or interface that is identified by its type. Generally the code who implements the service and the code who consumes it know nothing about each other. Cider accesses all services through the editing context, and there is only one editing context for each designer, so everyone has access to the same set of services (no more groveling for the right IServiceProvider instance). The editing context offers services through an instance of a class called ServiceManager. ServiceManager has some features not found on IServiceProvider, and a few behavioral differences too. Notably:

  • You can “subscribe” to a service. This allows you to be notified when a service becomes available. This feature becomes very interesting when coupled with a feature I’ll talk about in a later post which allows you to declaratively state which services a piece of code needs, allowing you to delay activate that code until all the services it needs to function are present.
  • You can enumerate services. Yes, no more black box. The service manager implements IEnumerable<Type> so you can easily peer into it to see what’s happening.
  • Services are forever. Once added, a service cannot be replaced or removed without disposing the entire editing context (which closes the designer). While this is less flexible than IServiceContainer, it is far more predictable and I think it will help Cider be more reliable.
  • The service manager does not walk upwards looking for services if they can’t be resolved locally. This is a large departure from IServiceProvider, where it was common to check your local stash of services and if you couldn’t find a match you’d ask your parent. Cider doesn’t do that for the same reason it doesn’t allow you to remove services: simplicity and predictability are more important to us than being overly flexible.

Services make up a very important half of the story. Context items make up the other half. Let’s talk about those next.

Context Items

Context items are a new concept in Cider. When I started working on Cider I did two things. First, I interviewed a lot of people who were using the IComponent based designer model to find what they liked and what the didn’t like. Second, I looked at all the classes in the Windows Forms designer to see if there were common patterns we could roll into the Cider infrastructure so they didn’t have to be implemented again and again.

One of the common patterns that emerged from the Windows Forms designer was this idea of a service whose sole goal was to maintain a bit of state and provide an event to let other objects know when that state changed. The best example of this is ISelectionService. This service gives you access to an array of selected objects, and offers an event to let you know when the array has changed. A similar pattern is duplicated all over the Windows Form designer (IHelpService, IDesignerHost’s transaction API, etc).

Context items provide a standardized mechanism for this type of pattern. A “context item” is an instance of a class that:

  1. Derives from ContextItem.
  2. Is Immutable.
  3. Overrides ContextItem’s ContextItemType property to provide the type that should be used when locating context items.

Let’s look at how the concept of selection works in Cider to see how context items are used. Selection in Cider is based on the value of a Selection object, which derives from ContextItem:

public class Selection : ContextItem {
    public Selection(params object[] selectedObjects);
    public sealed override Type ContextItemType
        get { return typeof(Selection); }
    }
    public IEnumerable SelectedObjects { get; }
    public object PrimarySelection { get; }
}

As you can see, there’s not much too it. It’s also pretty easy to use. If I wanted the selection to be “button1” I’d just do this:

Selection s = new Selection(button1);
editingContext.Items.Change(s);

If I wanted to know when selection changed I could subscribe to a change event on the editing context:

editingContext.Items.Subscribe<Selection>(delegate (Selection newSelection) {
    // selection changed, do something here.
});

Above I used a generic-based API to subscribe to the event, but non-generic events are supported too.

The true value of this model is that it can be extended. What if you had a designer that allowed the user to select text in a rich editor? You’d want a way for the selected text to be part of the selection. The only problem is that the Selection class I described above doesn’t have anywhere to store the text. No problem – just derive from the Selection class:

public class TextSelection : Selection {
    public TextSelection(string text, params object[] selectedObjects);
    public string SelectedText { get; }
}

Now if you want to set a text selection, pass a TextSelection object to the editing context. It will be seen as a selection (because it is), and anyone who is aware of text selection can check to see if the current selection contains text.

Well, that’s it for editing context. In my next installment I’ll talk about a layer that builds on top of the EditingContext class and sets the foundation for Cider’s extensibility model.


Viewing all articles
Browse latest Browse all 20

Trending Articles