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

LearnVSXNow! #30 - Custom Editors in VSXtra

When I started VSX programming with Visual Studio 2008 SDK, one of the most esoteric things were custom editors. The VSPackage wizard created a custom editor with a few thousands code lines, and it early came to light that Managed Package Framework does not have support for custom editors.

Several times I tried to refactor common functionality and create support types in order to make custom editor creation much easier than it is right now. In the LearnVSXNow series I published three blog entries on this topic (Creating a simple custom editor: Part #15, #16 and #17).

After three month of work on VSXtra I thought that I have everything to build my former experiences with custom editors and add appropriate support classes. In this post I show you the results.

Custom Editors in Visual Studio

It is not surprising that MPF does not directly support custom editors, because they are quite complex entities and interact with many other entities and services within the Visual Studio IDE. I have not enough time and experience to enumerate all of them, so here are just a short list of possible interactions:

—  Custom editors are used to work on “Documents” as their subject is called in the Visual Studio terminology. Documents have their content what we call “Document Data” and can have one or more visual representation of this data called “Document View”. For example, a Windows form can have code and design views, an XML Schema can be seen as text or diagram.

—  Document Data can be persisted in many ways: the data can be stored to (and of course read from) files, databases, URLs on a remote computer, to a device, etc. What we take into account as one document can be stored in one or more file. For example, a Windows form can be stored into two .cs file and one .resx file. On the other hand one file can store one or more document.

—  There are documents that can be saved into more than one file format, most known are bitmaps and pictures. The VS IDE allows the users to chose a save format for them. Custom editors may slightly modify their behavior depending on the file format used, for example the Image Editor modifies the color palette according to the color-depth of the image.

—  Custom editors may interact with the Property window indicating the current selection (object edited).

—  There are standard commands in Visual Studio IDE (like Cut, Copy and Paste). Custom Editors may sign which commands are accepted in the current context and even execute them.

Adding full support for all type of custom editors is not a reversionary thing: there are too many subtle details requiring a lot of investment. I decided to go on step-by-step and adding custom editor support for a few editor stereotypes. As one of the most common things first I decided to add support for single-file editors with a custom file extension. At the first stage I wanted to implement custom editors supporting only one document view called “primary view” in the VS terminology.

I implemented the Blog Item Editor (I have already introduced one example of this editor in LearnVSXNow Part #15-17). This time I aimed more compact solution (less code lines).

The Blog Item Editor

The example I have created is a simple WinForms-based editor that allows editing a dummy blog post entry with three text items (Title, Categories and Body). The editor stores the blog entries into an XML file having the .blit extension. The following figure illustrates the Blog Item Editor in work:

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:

<?xml version="1.0" encoding="utf-8"?>

<BlogItem xmlns="http://www.codeplex.com/VSXtra/BlogItemv1.0">

  <Title>My new blog item</Title>

  <Categories>

    <Category>VSX</Category>

    <Category>Visual Studio 2008</Category>

  </Categories>

  <Body>

    <![CDATA[VSXtra now supports creating Custom Editors in Visual Studio.

This example implements a blog item editor.]]>

  </Body>

</BlogItem>

Architecture

The Blog Item Editor’s implementation contains the following core types:

The BlogItemEditorFactory is responsible to create the editor instances when Visual Studio requests them. The BlogItemEditorPane takes the lion’s share in the whole custom editor functionality. Visual Studio perceives that BlogItemEditorPane implements both the document data and document role functionalities. The pane of the editor assigns document persistence tasks to the BlogItemEditorData class, UI responsibilities to the BlogItemEditorControl class.

If you have read Part #15, you can recognize this architecture is the same as I described there. However, since that time I changed a lot the support behind the editor factory and the editor pane. VSXtra contains the following abstract classes to help the creation of custom editors:

EditorFactoryBase<TPackage, TEditorPane> class

This class is intended to be the base classes of all editor factories. The TPackage type parameter names the package that owns the editor factory; it must be a VSXtra.PackageBase derived type. TEditorPane tells the factory the type of the editor pane undertaking the responsibility of the document data and document view. Including TPackage as a type parameter in the editor factory allows automatic registration of the factory—without the need of writing any explicit registration code.

EditorPaneBase<TPackage, TFactory, TUIControl> class

This abstract class implements the majority of tasks a simple file-persisted custom editor should have. The class directly derives from the WindowPane<TPackage, TUIControl> type, where TPackage represents the package owning the editor; TUIControl is a placeholder for the WinForms-based UI of the editor. TFactory refers to the factory class of the custom editor. By inheriting from WindowPane<,> the editor pane class gets the support for command handler methods.

ICommonEditorCommand interface

Majority of custom editors wants to support the most frequently used editing commands (Select All, Cut, Copy, Paste, Redo and Undo) in its own way. The ICommonEditorCommand interface declares this behavior for this six editor commands. The interface declares two members for each command:

—  A bool property with the Supports prefix to check if the command is available in the current context

—  A method with the Do prefix to execute the command—assuming, it is available

EditorPane<,,> implements this interface so that it examines the TUIControl type parameter. If TUIControl implements ICommonEditorCommand, the editor pane propagates the interface methods to the TUIControl instance.

Creating the Package implementing the Blog Item Editor

The package implementing the Blog Item Editor is simple:

namespace DeepDiver.BlogItemEditor

{

  [PackageRegistration(UseManagedResourcesOnly = true)]

  [DefaultRegistryRoot("Software\\Microsoft\\VisualStudio\\9.0")]

  [InstalledProductRegistration(false, "#110", "#112", "1.0",

    IconResourceID = 400)]

  [ProvideLoadKey("Standard", "1.0", "BlogItemEditor", "DeepDiver", 1)]

  [ProvideEditorFactory(typeof(BlogItemEditorFactory), 200,

    TrustLevel = __VSEDITORTRUSTLEVEL.ETL_AlwaysTrusted)]

  [ProvideEditorExtension(typeof(BlogItemEditorFactory),

    BlogItemEditorPackage.BlogFileExtension,

    32,

    ProjectGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}",

    TemplateDir = @"..\..\Templates",

    NameResourceID = 200)]

  [ProvideEditorLogicalView(typeof(BlogItemEditorFactory),

    "{7651a703-06e5-11d1-8ebd-00a0c90f26ea}")]

  [Guid(GuidList.guidBlogItemEditorPkgString)]

  public sealed class BlogItemEditorPackage : PackageBase

  {

    public const string BlogFileExtension = ".blit";

  }

}

Its body does not contain any real code; it only defines a string constant for the file extension. To tell Visual Studio how to find custom editor information in the registry, the ProvideEditorFactory, ProvideEditorExtension and ProvideEditorLogicalView attributes should decorate the package. For details about them see Part #16.

Here you cannot see any explicit custom editor registration code in the source. The PackageBase class does it automatically behind the scenes.

Creating the Editor Factory

The editor factory class simply uses EditorFactoryBase with concrete type parameters:

[Guid(GuidList.GuidBlogEditorFactoryString)]

public sealed class BlogItemEditorFactory:

  EditorFactoryBase<BlogItemEditorPackage,BlogItemEditorPane>

{

}

Behind the scenes the GUID of the factory type is used, so it is a good practice to explicitly define the Guid attribute and not let the compiler to create one implicitly.

Creating the Editor Pane

The editor pane undertakes the co-ordination among the document view and document data. The majority of these tasks are implemented in the EditorPaneBase<,,> class and the concrete implementation handling the Blog Item Editor must override only a few methods:

public sealed class BlogItemEditorPane:

  EditorPaneBase<BlogItemEditorPackage, BlogItemEditorFactory,

  BlogItemEditorControl>

{

  private readonly BlogItemEditorData _EditorData = new BlogItemEditorData();

 

  public BlogItemEditorPane()

  {

    UIControl.ContentChanged += DataChangedInView;

  }

 

  protected override string GetFileExtension()

  {

    return BlogItemEditorPackage.BlogFileExtension;

  }

 

  protected override void LoadFile(string fileName)

  {

    _EditorData.ReadFrom(fileName);

    UIControl.RefreshView(_EditorData);

  }

 

  protected override void SaveFile(string fileName)

  {

    UIControl.RefreshData(_EditorData);

    _EditorData.SaveTo(fileName);

  }

 

  void DataChangedInView(object sender, EventArgs e)

  {

    OnContentChanged();

  }

}

The concrete editor pane uses a BlogItemEditorData instance to store the document data. This instance is used in the SaveFile and LoadFile methods to store and recall the editor data. In order to tell the editor pane which file type is used to store blog item information we override the GetFileExtension method. Visual Studio must keep track of the “dirtiness” of the editor content and so we subscribe to the ContentChanged event of the UI. When this event is fired the OnContentChanged method of the editor pane is called to notify Visual Studio about “dirtiness”.

The UI of the Blog Item Editor

The source code for the UI supporting the Blog Item Editor overtakes the responsibility to handle common editor commands:

public partial class BlogItemEditorControl :

  UserControl,

  ICommonEditorCommand

{

  public BlogItemEditorControl()

  {

    InitializeComponent();

  }

 

  public void RefreshView(BlogItemEditorData data)

  {

    TitleEdit.Text = data.Title ?? string.Empty;

    CategoriesEdit.Text = data.Categories ?? String.Empty;

    BodyEdit.Text = data.Body ?? String.Empty;

  }

 

  public void RefreshData(BlogItemEditorData data)

  {

    data.Title = TitleEdit.Text;

    data.Categories = CategoriesEdit.Text;

    data.Body = BodyEdit.Text;

  }

 

  bool ICommonEditorCommand.SupportsSelectAll

  {

    get { return false; }

  }

 

  bool ICommonEditorCommand.SupportsCopy

  {

    get { return ActiveControlHasSelection; }

  }

 

  bool ICommonEditorCommand.SupportsCut

  {

    get { return ActiveControlHasSelection; }

  }

 

  bool ICommonEditorCommand.SupportsPaste

  {

    get { return ActiveCanPasteFromClipboard; }

  }

 

  bool ICommonEditorCommand.SupportsRedo

  {

    get { return false; }

  }

 

  bool ICommonEditorCommand.SupportsUndo

  {

    get { return false; }

  }

 

  void ICommonEditorCommand.DoSelectAll()

  {

    throw new NotImplementedException();

  }

 

  void ICommonEditorCommand.DoCopy()

  {

    var active = ActiveControl as TextBox;

    if (active != null) active.Copy();

  }

 

  void ICommonEditorCommand.DoCut()

  {

    var active = ActiveControl as TextBox;

    if (active != null) active.Cut();

  }

 

  void ICommonEditorCommand.DoPaste()

  {

    var active = ActiveControl as TextBox;

    if (active != null) active.Paste();

  }

 

  void ICommonEditorCommand.DoRedo()

  {

    throw new NotImplementedException();

  }

 

  void ICommonEditorCommand.DoUndo()

  {

    throw new NotImplementedException();

  }

 

  public event EventHandler ContentChanged;

 

  private void RaiseContentChanged(object sender, EventArgs e)

  {

    if (ContentChanged != null) ContentChanged.Invoke(sender, e);

  }

 

  private void ControlContentChanged(object sender, EventArgs e)

  {

    RaiseContentChanged(sender, e);

  }

 

  private bool ActiveControlHasSelection

  {

    get

    {

      var active = ActiveControl as TextBox;

      return active == null ? false : active.SelectionLength > 0;

    }

  }

 

  private bool ActiveCanPasteFromClipboard

  {

    get

    {

      var active = ActiveControl as TextBox;

      return (active != null && Clipboard.ContainsText());

    }

  }

}

The RefreshView and RefreshData methods provide the support to synchronize the editor data and the editor view. BlogItemEditorControl implements the ICommonEditorCommand interface and so the editor pane propagates the responsibility to the UI to handle those commands. The explicit ICommonEditorCommand implementation methods handle the Cut, Copy and Paste commands. When the text changes in any text box on the UI, the ControlContentChanged method is called raising the ContentChanged event. The editor pane subscribes to this event and so can notify Visual Studio about the change in the document data.

Document Data of the Blog Item Editor

The BlogItemEditorData class is responsible to hold the editor data in memory and also to provide persistence support. The class implements the IXmlPersistable interface and uses the .NET Framework v3.5 System.Linq.Xml types to implement this support.

public sealed class BlogItemEditorData : IXmlPersistable

{

  public BlogItemEditorData()

  {

  }

   

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

  {

    Title = title;

    Categories = categories;

    Body = body;

  }

 

  public string Title { get; set; }

  public string Categories { get; set; }

  public string Body { get; set; }

 

  public void SaveTo(string fileName)

  {

    // --- Implementation omitted

  }

 

  public void ReadFrom(string fileName)

  {

    // --- Implementation omitted

  }

 

  public void SaveTo(XElement targetElement)

  {

    // --- Implementation omitted

  }

 

  public void ReadFrom(XElement sourceElement)

  {

    // --- Implementation omitted

  }

}

Where we are?

VSXtra did the first step to add support to MPF for creating custom editors. The first concept VSXtra uses focuses on single-view/single file custom editors. The three main types intended to be the corner stones are EditorFactoryBase<,>, EditorPaneBase<,,> and ICommonEditorCommand.


Posted Sep 01 2008, 10:46 AM by inovak
Filed under: ,

Comments

Visual Studio Hacks wrote Visual Studio Links #70
on Tue, Sep 2 2008 15:11

My latest in a series of the weekly, or more often, summary of interesting links I come across related to Visual Studio. Sara Ford's Tip of the Day #303 covers the QuickWatch window . Carlos Quintero posted The diagram of the convoluted build configuration

GA30 wrote re: LearnVSXNow! #30 - Custom Editors in VSXtra
on Tue, May 19 2009 9:59

Hi inovak!

I am using the framework for a custom editor I am working on for Silverlight projects. When my file type is added to the project, my package needs to programmatically add a reference to some silverlight assembly. I thought about doing it via the vstemplate file I am using but I don't think this will work because SL assemblies don't go in the GAC. So my question to you is where in the lifetime of the editor should this logic go? Who's responsibility do you think it is to add the reference? Thank you

Clomiphene 50 mg wrote re: LearnVSXNow! #30 - Custom Editors in VSXtra
on Mon, Feb 25 2013 11:44

hNpDow Im thankful for the blog article.Much thanks again. Much obliged.