Dotneteers.net
All for .net, .net for all!

LearnVSXNow! #15 - Creating a simple custom editor — the basics

After dealing with menus and commands I take a break to show you some new topics related to custom editors. As we develop applications we use programming languages with text editors to define the code to be compiled into our product. Actually, we could solve all software development tasks with text editors, but we like other kind of editors like a Winforms editor or an ASP.NET page editor since they make our work much fun and much productive.

Visual Studio IDE allows us to create editors of our own to extend productivity. To create a custom editor requires much more work than creating tool windows, since a custom editor has much more interaction point with the VS IDE than a tool window has.

In the first parts of this series I used the VSPackage wizard to create simple examples. I could have asked the VSPackage wizard in VS 2008 to create a custom editor for me and I would be able just go through the code and explain you how it works and how to extend it.

This time I chose other approach. The main reason is the code created by the VSPackage wizard: it has about five thousand of lines dealing with the custom editor. It is much longer than really required and can be explained within a few articles. However, there is another reason: the code generated by the wizard is not free of bugs, it often shows up “The operation could not be completed” messages (that means it raises unhandled exceptions). In the sample of VS 2008 SDK I have found an example (C# Example.EditorWithToolbox) that is a very good starting point for talking about custom editors. I got through this example, collected the information about the custom editors and later could make a lot of refactorings. As a result I managed to cut the original fifteen hundred lines of code into a few generic classes to make you able to write a simple custom editor in about a hundred lines of code.

In this article I show you the basic architecture behind custom editors and give you an overview about the example I created. We start to dive into the code and go on with this “under-surface” activity in the next articles.

Editors in Visual Studio

All of us uses and so all us of know that Visual Studio has text editor, form editor and a few others. These are all internal editors.

With the Tools|External Tools menu function we can add external editors to our Visual Studio environment. Even we add them logically to Visual Studio, when we start them they run in a separate process (as they are separate .exe files).

We can use a few external editors within the editor windows just as if they were run in Visual Studio. A typical example is Microsoft Word 2007 or Excel 2007 in VSTO projects. Actually they are hosted as ActiveX controls in a VS document window. Making these kind of editors requires a lot of task to solve and we must know many other VSX internals (like custom projects, hierarchies, etc.) to create such kind of hosted editors.

In this article we are going to treat only internal editors that run within the devenv.exe process. Would you think or not, internal editors can have a variety of user interface styles:

—  Single view (window) editors. These are the most common (and simple) forms of editors. The data representing the information behind the editor (or designer) has exactly one view. When we edit a C# class file we use the text editor as a single view of the file.

—  Multiple view (window) editors. The data edited has multiple different views. An example is the Windows Forms editor. While editing a form file, we can switch between the form view and the code view. Even the data behind the designer (in our case behind the form) can be in multiple files. Depending on the action we do, we can change the content of one or more files. At the same time we can see both the form and the code window (for example we organize them into horizontal or vertical tab groups).

—  Multi-tabbed (simple window) editors. The data we handle has one or more views but in the same document window on separate tabs. A good example for that is the ASP.NET page editor when we can see the designer representing our page as we’ll have it in a browser and also we have a HTML view. The main difference between the multi-tabbed and the multi-window view is that the tabbed view generally provides a fashion where we see only one of the views provided by the editor and we cannot separate them into different windows.

—  Visual Studio also provides possibility to write editors bound to a hierarchical view in a tool window. Generally they require so-called custom projects where the editor has some special contract with the project type. An example is the SQL data editor. When we register a data connection to a SQL Server database in the Server Explorer window, we use this kind of hierarchical editors. Treating them is not the topic of this article.

Custom Editor Architecture

In the introduction I used the term “editor” and “designer”. In some articles and in the VSX documentation you can meet also with both of them. These two terms are interchangeable, so if you read them you know they cover the same idea maybe emphasizing different user perceptions.

The architecture behind the editors uses the MVC principles. To look over its main elements, the following figure will help us a lot:

Just like tool windows, custom editors are objects owned by a VSPackage. The VS Shell provides a service called SVsRegisterEditors that allows a package registering an editor. Actually it means registering a so-called editor factory (represented by an object implementing the IVsEditorFactory). The main reason the factory pattern is used that an editor itself divides its responsibilities among a few objects. The editor factory is the guy who knows how to create and initialize these objects—taking into account the current context.

When I am talking about responsibilities of an editor those are ones like responding to commands (for example Save, Cut, Copy and Paste) arriving from the VS IDE, displaying the user interface of the editor, handling the program code, XML documents or whatever is behind the editor, and so on. Actually, we VS manages two set of responsibilities:

—  Document View. Our editor has some user interface to allow users interacting with the editor. An editor generally has only one view, but can have two or more. A good example is the ASP.NET webpage editor that has a WYSIWYG designer and a HTML (text) editor, or the XML schema editor that has a graphical and a textual (XML) designer. On the picture above Document View 1 and 2 represents two separate views.

—  Document Data. There is no reason to edit something without its data representation. The document data is the information we are editing with our custom editor. When we load our editor, this data is loaded into the memory, when we save the document this data is persisted into a file. Actually, VS does not require the data to be persisted in a file, but for this article we go on this thread.

Visual Studio does not require the document view and document data roles to be separated into two or more object instances. One editor instance may be smart enough to undertake the responsibility of the view and the data. It is always a design question how to separate the roles among objects.

When the editor factory created the objects representing the editor, the VS Shell (the services behind the shell) can use them to interact with through the implemented service end points. In the following table I summarize the most important service end points with their responsibility and general usage form.

Service end point Responsibility, description
IVsEditorFactory

This interface provides services to create and initialize view and document objects used by the editor. Of course, when closing the editors some cleanup activity is required to release resources held by the editor, so this cleanup service is also provided by IVsEditorFactory. As we have seen, this service endpoint is required by the editor factory object.

IOleCommandTarget

Both the document view and the document data must understand commands coming from the environment (VS IDE or other registered packages). For example, the view can understand and execute the Copy and Paste commands. The document data is required to handle the load and save commands. The IOleCommandTarget interface defines this responsibility.

(In Part #13 we treated the idea of command target. This is exactly the behavior defined by IOleCommandTarget. In future articles we’ll meet with it many times, but I think at the end of this article you will have a very good understanding what it does and how it does.)

IVsWindowPane

Just like other windows in the VS IDE, editor windows can be moved, docked, pinned or unpinned, put into tabs and so on. If we create a user interface for our editor, all this windowing tasks are provided by the shell. To leverage on them our document view must implement the IVSWindowPane interface.

(In Part #4 we have already met with IVsWindowPane when we created our tool window. No surprise, VS Shell provides the same services for all windows...)

IVsPersistDocData

This set of services is responsible to manage document data persistence (to load it into the memory and save it sowhere). We might think it is a simple interface, but in reality it provides about ten methods. Just to picture that it makes a complex task, let me tell you a few things about it:

—  The document itself can be persisted to any kind of abstract storage. This interface provides this kind of abstraction.

—  The user can modify the document outside of Visual Studio. This situation is recognized and the user can reload the modified document.

—  The file holding the document can be renamed in Visual Studio or saved with the Save as... function.

IPersistFileFormat

Generally data behind editors is persisted to files. A document can be load from or saved to different file formats. This interface deals with handling functions related to these tasks.

When we use Visual Studio we generally edit more than one document. To understand how VS Shell handles them, we must look into a few internals.

The Running Document Table

As I treated a few paragraphs before, editor have document data and one or more document views working on the data. While en editor is not open in Visual Studio, it has no open views and its data is not affected: the data is simple sits in a file or in a database. However, when an editor is open, it means there is a view working on its data. If the editor has multiple views open, there some coordination must be done to synchronize the views.

There are editors where it is quites easy to do: let’s assume we have a database table designer when we can represent a table’s fields in a grid (Grid View) and in a text editor (DDL View). Where we modify the table information in one place, it is quite easy to represent the data in the other view.

There might be editors where it is much more difficult, a good example is the Windows Form editor. When we do some change in the graphical designer, changes are only synchronized in the textual view (in the designer code text) when we save the form.

Visual Studio uses the Running Document Table (RDT) to manage open documents. When we change a document data, it helps to administer what views and what files (or what other persistence elements, for example database tables, stored procedures, etc.) are modified. When we close a file or even the open solution, the RDT is the infrastructure element in the background that helps Visual Studio to pop-up the Save changes window:

Every item we see in the window above is a document in the RDT. A document is something that should be saved (persisted) as a whole undivided unit. If you decide that your application stores everithing in one file (even it is represented in the Solution Explorer as a hierarchy of items), than you have one document. If you decide that one item in the Solution Explorer is represented as two separate files, you have two documents.

RDT used so-called edit locks to coordinate the usage of open documents. When an editor is opened, it opens a view to edit the document. At this time the document goes into the RDT and an edit lock is put on that document. If another view is opened for the document, a new edit lock is put to the document. This is implemented so that RDT uses lock counts. So when the second view is opened, the lock count becomes two.

RDT watches the edit lock count transitions. When the counter goes to zero, Visual Studio prompts us to save the changes. Going back our sample, when we close any of the views from the two opened before, the lock count decrements to 1. It means there is one view already using the document. When we close the second view, the lock count goes to zero and Visual Studio prompts us.

Not only editors and view can open a document. We might use an add-in or a package that generate code snippets with the help of a tool window. When initiating an action, the add-in might invisibly open project files (without opening an editor with views) and put code snippets into them. When the add-in opens files (if it does it through the corresponding APIs of VS), the edit lock count in RDT is also maintaned.

So, what is stored in the RDT? Actually every information that is required to manage the document and its holders:

—  File path or equivalent URI. When our document is persisted in a file, the RDT must know its full path to provide an “address” when it is accessed. If the document is a database, an item in a logical store (e.g. a record in a databas table, etc.) some URI-like information is stored as an “address”.

—  A pointer to the object representing the document data in memory. Through this object we can not only access the document data but even check its state (for example, if it is “dirty”, since we modified after opening it).

—  Document flags. Some simple flags influencing document behaviour (e.g. “Do not save this document”, “Do not open it next time when the solution is opened”, etc.)

—  Edit locks and read locks.

—  The owner hierarchy of the document. Each document has an owner in a hierarchy. Generally this is one of the file nodes of the Solution Explorer, but of course it can be any other hierarchy. For example, a SQL database in the Server Explorer also has a hierarchy. This hierarchy is the owner of a database table document. The role of owner is important, since VS Shell does not saves the document directly, it always turns to the owner and asks it to save.

—  A list of pointers to “invisible” lock holders. I mentioned that add-ins and packages might have open documents invisibly. The RDT keeps a list of this kind of lock holders to notify them about events through the IVsDocumentLockHolder interface the lockers implement.

Editor priorities

Editors belong to a file extension. When we register them we also register the file extension them and a property called priority. This priority is important, since it is used by the Shell to find the best editor for a certain file extension. Editors can register file extensions with wildchars (even can register themselves for “.*”.

When Visual Studio opens a file, checks the editors registered for a matching file extension.  Then it goes through the list of potential editors in priority order (starts with the highest priority). The editor has the opportunity to make a decision if it can (and wants) to handle the file. If it accepts the file, Visual Studio says “yes, you’ve got it”. If the editor refuses the file, Visual Studio goes to the subsequent editor on its list while it does not find one.

How can it be ensured that there is at least one editor that handles a certain file? The solution is provided by Visual Studio built-in editors: there a re a few editors (for example the binary editor and the XML editor) that registers itself to “.*”. Due to this fact what is happening where there are no specific editors for a certain file is similer to the folllowing scenario:

There are many developers using XML files, so if there is no other editors handling a file, somewhere at the end of the priority list the XML editor tries to open the file. If it recognises that is an XML feli, the editor accepts and loads it. If the file is not an XML file, then the binary editor opens it.

The BlogItemEditor sample

Now it’s the time to see in practice how to create a custom editor. It is a much more complex topic than creating a tool window since we have more functionality behind an editor (if I say designer you can imagine why this functionality is so complex). In the introduction of this article I mentioned that I was not really satisfied with the custom editor code created by the VSPackage wizard, since it was too long with its five thousand lines of code to explain. However, the VS SDK contained the C# Example.EditorWithToolbox sample that was a perfect starting point.

I examined the sample code and established my own example of a simple custom editor. The original sample used a single RichTextEdit control to provide its functionality, but it is not a frequently used example.

I called my example BlogItemEditor having a simple (but multi-control) user interface like this:

The editor handles a simple blog item with a title, a list of categories and of course with a body. I implemented the editor to be able to persist the blog item as an XML file (I did not implement any functionality to upload it to a blog engine). I imagined my XML format to be able to provide multiple XML elements for categories string (for example categories are separated by semicolons) and blog post body represented by a CDATA element:

<BlogItem xmlns=”...”>

 <Title>Sample Blog Item</Title>

 <Categories>

   <Category>VSX Sample</Category>

   <Category>Visual Studio 2008</Category>

   <Category>LearnVSXNow!</Category>

</Categories>

<Body>

  <![CDATA[

  After dealing with menus and commands I take a break to show you some new

  topics related to custom editors. As we develop applications we use programming

  languages with text editors to define the code to be compiled into our product.

 

  ...

  Where we are?

  ...

  ]]>

</Body>

</BlogItem>

The architecture of the solution

I created the custom editor with four core types as illustrated in the following diagram:

The BlogItemEditorFactory class does what we expect from an editor factory. I separated the document view and the document data roles into three types. The BlogItemEditorPane undertakes the majority or tasks we expect from a document view and from a document data.  The BlogItemEditorControl class that provides user interface for the editor and provides a part of functions what the controller does in the MVC pattern. The BlogItemEditorData stores the information in the memory and is able to persist itself from and to ax XDocument (new style for managing XML documents, see the System.Xml.Linq namespace).

If you look at the user interface prototype, the XML data representation and the simple class diagram above, you may have to the same idea I had when thinking it over:

We can make a bunch of reusable code for simple editors with a form-like user interface persisting in-memory data into XML files. I factored out four reusable types and put them into the VsxLibrary:

Type Role
SimpleEditorFactory<TEditorPane>

Provides an editor factory that creates en editor where the document view and the data view is represented by a TEditorPane type.

SimpleEditorPane<TFactory, TUIControl>

Type undertaking the role of a document view and a document data. This type is instantiated by a TFactory editor factory. The user interface is represented by a TUIControl user control.

ICommonCommandSupport

The interface represents the behavior of a user interface that supports standard editor commands like Select All, Copy, Paste, etc.

IXmlPersistable

A simple interface describing Save and Load operations for an instance of XElement.

To understand how these out-factored element help in the BlogItemEditor example, let’s see the details.

BlogItemEditorFactory

Due to the fact that the SimpleEditorFactory<> generic type does everything an editor factory should do, BlogItemEditorFactory has quite simple code:

[Guid(GuidList.guidBlogEditorFactoryString)]

public sealed class BlogItemEditorFactory:

  SimpleEditorFactory<BlogItemEditorPane>

{

}

It will use the BlogItemEditorPane as the view and the data of the editor. As for any other object used by the VS services we need a Guid for the factory.

BlogItemEditorData

This class represents the data content (in memory representation) of a blog item. This type has the following blueprint:

public sealed class BlogItemEditorData : IXmlPersistable

{

  public BlogItemEditorData(string title, string categories, string body)

  { ... }

  public string Title { get; }

  public string Categories { get; }

  public string Body { get; }

  public void SaveTo(string fileName) { ... }

  public void ReadFrom(string fileName) { ... }

 

  // --- IXmlPersistable implementation

  public void SaveTo(XElement targetElement) { ... }

  public void ReadFrom(XElement sourceElement) { ... }

}

The Title, Categories and Body properties are used to read the content of the blog item set by the constructor. We have SaveTo(XElement) and ReadFrom(XElement) methods implementing the IXmlPersistable interface. These methods use XElement arguments rather than XDocument arguments since with design you can compose separate IXmlPersistable objects into one XDocument.

The SaveTo and ReadFrom methods with a string argument simply use the XElement content and save it to a file or load it from.

BlogItemEditorControl

This user control implements the user interface of our blog editor. Allows the user interacting with it and provides support for the most common Visual Studio commands:

public partial class BlogItemEditorControl :

  UserControl,

  ICommonCommandSupport

{

  public BlogItemEditorControl()

  {

    InitializeComponent();

  }

  // ...

  // --- ICommonCommandSupport implementation

  // ...

}

ICommonCommandSupport

This interface defines the behavior a user control must provide to add basic support for the most common editor commands. The definition of this interface tells what this support is:

public interface ICommonCommandSupport

{

  // --- Support flags

  bool SupportsSelectAll { get; }

  bool SupportsCopy { get; }

  bool SupportsCut { get; }

  bool SupportsPaste { get; }

  bool SupportsRedo { get; }

  bool SupportsUndo { get; }

 

  // --- Command execution methods

  void DoSelectAll();

  void DoCopy();

  void DoCut();

  void DoPaste();

  void DoRedo();

  void DoUndo();

}

Properties having the Supports prefix tell the environment whether the implementer object supports the command named in the property. If the environment finds the object supports the command it will call the appropriate Do-prefixed method to execute that command.

BlogItemEditorPane

The major part of the whole editor work is done by the BlogItemEditorPane class which represents both the document view and data. However, its code is really simple:

public sealed class BlogItemEditorPane:

  SimpleEditorPane<BlogItemEditorFactory, BlogItemEditorControl>

{

  public BlogItemEditorPane() { ... }

  protected override string GetFileExtension() { ... }

  protected override Guid GetCommandSetGuid() { ... }

  protected override void LoadFile(string fileName) { ... }

  protected override void SaveFile(string fileName) { ... }

}

The key of this simplicity is the SimpleEditor<,> generic type that takes two type arguments. The first type is the editor factory creating the view and data for this editor. The second type is the user control defining the user interface of the editor. When deriving our editor pane from SimpleEditor<,> we need only override four abstract methods:

Method Role
GetFileExtension

Defines the file extension used by our editor.

GetCommandSetGuid

Defines the GUID identifying the command set handled by the editor.

LoadFile

Loads the data content of the editor from a file.

SaveFile

Saves the data content of the editor into a file.

It is not a secret, the longest code belongs to the SimpleEditorPane<,> type. Later we’ll see it in details, now let’s have a look for its type definition:

public abstract class SimpleEditorPane<TFactory, TUIControl> :

  WindowPane,

  IOleCommandTarget,

  IVsPersistDocData,

  IPersistFileFormat

  where TFactory: IVsEditorFactory

  where TUIControl: Control, ICommonCommandSupport, new()

{

  // ...

}

SimpleEditorPane implements all the key interfaces required for a custom editor. By inheriting from WindowPane we got the IVsWindowPane implementation. The class implements the IOleCommandTarget to be able process commands; IVsPersistDocData and IPersistFileFormat to handle persistence functions. It accepts only real factory types implementing IVsEditorFactory as the TFactory type argument. Expects user controls implementing the ICommonCommandSupport interface as the TUIControl type argument.

Where we are?

We started to develop a simple custom editor. Before going into the concrete example we examined the basic architecture behind editors in VS. Editors (represented by editor factories) should be registered in Visual Studio in order to use them with associated file types. Editors have concepts for document data and document views. An editor may have one or more views, all views working on the same data.

The Running Document Table is the infrastructure element in the VS IDE that is responsible for managing open documents (document data) behind editors. It implements a simplelocking mechanism to manage saving (or persisting) documents from memory to their physical storage. Registered editors have priorities and Visual Studio find the appropriate (“best”) editor for a certain file using this priority information.

In the article I introduced the basic elements of a BlogItemEditor sample. In the next article we take a deep dive into the code starting with editor factories.


Posted Mar 12 2008, 06:55 PM by inovak
Filed under:

Comments

DiveDeeper's blog wrote LearnVSXNow! #30 - Custom Editors in VSXtra
on Mon, Sep 1 2008 10:51

When I started VSX programming with Visual Studio 2008 SDK, one of the most esoteric things were custom

???????????? | VsxHowTo-???Windows Forms Designer???????????????????????????1??? wrote ???????????? | VsxHowTo-???Windows Forms Designer???????????????????????????1???
on Thu, Jan 27 2011 5:38

Pingback from  ???????????? | VsxHowTo-???Windows Forms Designer???????????????????????????1???