240
一线老司机

(翻译)LearnVSXNow! #15- 创建简单的编辑器-基础

     在了解了菜单和命令之后,我们接下来的几篇文章将以自定义编辑器为主题。在开发程序的时候,我们可以用文本编辑器来编写程序代码,并且实际上我们可以用文本编辑器完成所有的开发工作,但我们通常不这么做,因为在visual studio中有很多可以提高我们效率的编辑器,例如winforms编辑器和asp.net的页面编辑器。

     Visual Studio IDE允许我们创建自己的编辑器。但创建一个自定义编辑器要比创建一个Tool window复杂多了。

     利用VSPackage向导,可以帮助我们创建一个自定义编辑器,但我不打算利用VSPackage向导。这是因为向导生成的代码太长了:光编辑器就有差不多有五千行的代码,但实际上并不需要这么多代码。另外一个原因是:向导生成的代码是有bug的,经常会报出“The operation could not be completed”的异常。不过,vs2008 sdk的例子(C# Example.EditorWithToolbox) 是比较好的,大家可以从这里入手。

     这篇文章主要讲述编辑器的基本结构,并且给出一个我做的例子。

Visual Studio中的编辑器

     众所周知,Visual Studio里有文本编辑器、表单编辑器等等,它们都是内部的编辑器,因为它们运行在Visual Studio的进程里。通过工具|外部工具(Tools|External Tools ),我们可以添加外部的编辑器,但它们实际上是运行在独立的进程里的,是独立的exe文件。

     当然,也有一些外部编辑器看起来像是运行在Visual  Studio里面,例如VSTO项目中的Microsoft Word 2007或者Excel 2007。但它们实际上是作为ActiveX控件嵌入在VS的Document Window里的,实现这种编辑器要做的工作太多了。

     我们只讨论运行在devenv.exe这个进程中的编辑器。这种编辑器有多种类型:

  1. 单视图(Single view )编辑器。这种是最常见的编辑器。例如C#的代码编辑器。
  2. 多视图(Multiple view )编辑器。正在编辑的数据有多个视图。例如winform的表单设计器,它包含设计视图和代码视图,表单背后的代码甚至可以存放在多个文件里。我们在设计器里的一个动作会同时修改多个文件。并且,我们可以同时看到设计视图和代码视图(可以通过新建水平选项卡或者垂直选项卡)。
  3. 多页签(Multi-tabbed )编辑器。正在编辑的数据有多个视图,但是这些视图存在于同一个Document Window中。例如ASP.NET的页面编辑器,它包含设计视图和html视图,但它和多视图(Multiple view )编辑器最大的不同是,这些视图是位于不同的页签里的,而不是不同的窗口里。
  4. Visual Studio也允许我们创建和工具窗(Tool Window)绑定的编辑器。例如SQL Data Editor,当我们在Server Explorer里连接到一个数据库之后,就可以用SQL Data Editor了。但这种编辑器不是我们这篇文章讨论的内容。

编辑器的结构

     编辑器的结构符合MVC结构,下图可以帮助我们了解它的主要结构:

EditorArchitecture2

     和工具窗(Tool Window)一样,自定义编辑器也是从属于VSPackage的。package可以用由vs shell提供的SVsRegisterEditors服务来注册编辑器,实际上注册的是一个实现了IVsEditorFactory的编辑器工厂,这个工厂负责初始化编辑器。

     一个编辑器要实现很多功能,例如保存、剪切、复制、粘贴,还要负责显示编辑器的界面,维护编辑器要编辑的文件,等等等等。实际上有两类东西需要编辑器管理:

  1. 文档视图(Document View)。编辑器必须要有界面和用户交互。一个编辑器通常只有一个视图,当然也可以有两个或者更多,例如ASP.NET的webform编辑器有一个所见即所得的设计视图和一个html的源视图;再比如xml schema编辑器有一个图形视图和xml源视图。上图中的Document View 1和2就分别代表两个视图。
  2. 文档数据(Document Data)。做一个不需要编辑数据的编辑器是毫无意义的。当编辑器加载的时候,数据被加载到内存里,保存文档的时候,数据被保存到文件里。当然,VS实际上并不要求数据必须保存在文件里,不过我们这篇文章只关注数据保存到文件里的情况。

     文档视图(Document View)和文档数据(Document Data)不一定非得用两个类来实现,用一个类就已经足够了。要实现编辑器,还需要实现一些接口,如下表:

接口 说明
IVsEditorFactory

这个接口用来创建和初始化编辑器,以及在关闭编辑器时去清理资源。该接口对于实现编辑器是必须的。

IOleCommandTarget

Document view和Document data都必须识别由vs或其他package传过来的命令。例如,document view要识别和执行“复制”和“粘贴”命令,document data要识别“加载”和“保存”命令。IOleCommandTarget接口就是负责这个的。

(在第13章的时候,我们曾经提到过Command target,在以后的文章里我们还会多次见到它。)

IVsWindowPane

文档视图实现了IVsWindowPane接口之后,就可以像vs ide中的其他窗口一样,可以移动、停靠。

(我们第4篇文章曾经用这个接口创建了一个Tool window)

IVsPersistDocData

这个接口用来管理文档数据(例如把数据加载到内存里,或者把它存在某个地方),它大概有10个方法,比较复杂:

  1. 文档本身可以被持久化到任何地方。
  2. 用户可以在VS外面修改文档,在VS里重新加载修改后的文档。
  3. 可以在VS里重命名文件,或者“另存为”文件。
IPersistFileFormat

通常情况下,设计器对应的文档被持久化到文件里。一个文档可以有多种不同的文件格式。这个接口就是用来处理文件格式的。

Running Document Table

     编辑器拥有文档数据和一个或多个文档视图。在编辑器还没有被打开的情况下,文档数据只是被存放在文件或数据库(或其他地方)里,但是一旦打开了编辑器,就意味着至少有一个视图正在处理数据,如果编辑器有多个视图的话,还需要在多视图之间同步数据。

     例如数据库表的设计器有Grid视图和DDL视图,当我们修改了其中一个视图里的数据之后,数据会同步到另外一个视图。再比如Windows Forms设计器,当我们在设计视图做了些修改之后,会把代码同步到代码视图。

     Visual Studio利用Running Document Table(RDT)来管理打开的文档。当一个文档的数据改变之后,它可以判断哪些视图和哪些文件(或其他的持久介质,例如数据库的表、存储过程等等)被修改了。当我们关掉一个文件或者关掉解决方案的时候,RTD就会告诉Visual Studio,从而弹出一个询问我们是否要保存文件的对话框:

image

     在这个对话框里的每一项都代表RDT里的一个文档。文档是作为一个独立的单元来持久化的。例如,假设你打算把你的solution都存放在一个文件里的话,你就只有一个文档;假设你的解决方案存放在两个单独的文件里,那么就有两个文档。

     RDT利用所谓的“编辑锁”来协调已经打开的文档。当一个文档视图打开的时候,这个文档就被加到了RDT里,RDT给这个文档加了一个“编辑锁”,如果再打开这个文档的另一个视图,RDT又会给这个文档加一个新的“编辑锁”。所以当打开第二个视图的时候,锁的个数变为2。

     当一个文档对应的锁的个数变为0的时候,VS就会提示我们去保存文档。

     那么,RDT里到底存放了些什么东西呢?

  1. 文件的路径或uri。如果文档是存在文件里的,RDT必须知道它的绝对路径;如果文档是存在数据库里的,RDT也必须知道能够唯一定位该文档的地址。
  2. 文档数据在内存里的指针。利用该指针,我们不仅可以访问文档的数据,也可以用来检查数据的状态(例如该数据是否被修改过)。
  3. 文档的状态标记。例如“Do not save this document”, “Do not open it next time when the solution is opened”,等等。
  4. 拥有文档的节点(hierarchy)。通常是解决方案里的一个文件的节点,但也可以是其他类型的,例如服务器资源管理器里的数据库节点。文档的拥有者是很重要的,因为VS Shell不会直接保存文档,而是让拥有者来保存。
  5. 指向不可见的锁的指针列表。add-in和package可以用不可见的方式打开文档,RDT也会给这些打开的文档加锁。

编辑器的优先级

     编辑器是和文件的扩展名挂钩的,当注册编辑器的时候,需要指定优先级。优先级很重要,因为VS可以利用优先级来给某个特定扩展名的文件选用最佳的编辑器。

     当VS打开一个文件的时候,它通过该文件的扩展名找到对应的编辑器(不止一个),然后根据优先级来决定该使用哪一个编辑器。

     VS怎样才能确保至少有一个编辑器可以打开文件呢?这个难题由Visual Studio内置的编辑器来解决:VS内置了一些编辑器(例如二进制编辑器和XML编辑器),这些编辑器和“.*”文件挂了钩。所以,如果一个文件并没有特定的编辑器的话,就会用这些内置的编辑器打开它们。

BlogItemEditor示例

     说了这么多,终于该看一看怎样做一个自定义编辑器了。我的编辑器叫做BlogItemEditor,界面如下图:

image

     这个编辑器用来编辑简单的博客,有标题、分类和内容。用xml文件来存储(我并没有实现上传到博客引擎的功能)。分类是一个列表,可以有多个分类(在界面上用分号隔开),博客内容存为CDATA元素格式:

<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>

项目结构

我的编辑器用了4个核心的类,如下图:

image

     BlogItemEditorFactory是编辑器工厂。我把文档视图和文档数据分隔到3个类里面。BlogItemEditorPane 负责管理文档视图和文档数据,BlogItemEditorControl 是编辑器的界面,BlogItemEditorData 是数据的载体。

     我把创建一个简单的编辑器的代码封装了一下,放到了VsxLibrary里:

类型 作用
SimpleEditorFactory<TEditorPane>

编辑器工厂,负责创建编辑器

SimpleEditorPane<TFactory, TUIControl>

负责管理文档视图和数据,TFactory是编辑器工厂,TUIControl是界面。

ICommonCommandSupport

指示界面是否支持“全选”、“复制”、“粘贴”等功能。

IXmlPersistable

定义用于读取和保存XElement的方法。

 

BlogItemEditorFactory

     BlogItemEditorFactory 继承自SimpleEditorFactory<>泛型类,由于基类里已经做了创建编辑器的逻辑,所以这个子类就很简单了:

[Guid(GuidList.guidBlogEditorFactoryString)]
public sealed class BlogItemEditorFactory: 
  SimpleEditorFactory<BlogItemEditorPane>
{
}

     它用BlogItemEditorPane 作为编辑器的视图和数据。

BlogItemEditorData

     这个类用于处理数据:

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) { ... }
}

     Title、Categories和Body属性在构造函数里初始化;SaveTo(XElement)ReadFrom(XElement)IXmlPersistable接口的方法实现;另外两个带有字符串参数的SaveTo和ReadFrom方法负责保存和读取把BlogItemData。

BlogItemEditorControl

     这个用户控件就是我们的编辑器的界面了。它实现了ICommonCommandSupport接口:

public partial class BlogItemEditorControl : 
  UserControl,
  ICommonCommandSupport
{
 
  public BlogItemEditorControl()
  {
    InitializeComponent();
  }
  // ... 
  // --- ICommonCommandSupport implementation
  // ...
}

ICommonCommandSupport

     这个接口指示界面是否支持“全选”、“复制”、“粘贴”等功能,它的定义如下:

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();
}
 

     Supports为前缀的属性表示是否支持相应的命令,如果支持的话,就会调用相应的以Do为前缀的方法。

BlogItemEditorPane

     我们的编辑器的主要工作是由BlogItemEditorPane 来完成的,不过,它的代码是很简单的:

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) { ... }
e) { ... }
}

     基类SimpleEditorPane接受两个类型参数,第一个是编辑器工厂类,第二个是表示用户界面的用户控件,而我们的子类只需要重写下面几个虚方法:

方法名 描述
GetFileExtension

定义我们的编辑器支持的文件扩展名

GetCommandSetGuid

定义我们的编辑器可以处理的CommandSetGuid

LoadFile

读取数据

SaveFile

保存数据

     现在让我们来看一下基类SimpleEditorPane的类声明:(迟些我会仔细说明一下这个类)

public abstract class SimpleEditorPane<TFactory, TUIControl> : 
  WindowPane,
  IOleCommandTarget,
  IVsPersistDocData,
  IPersistFileFormat
  where TFactory: IVsEditorFactory
  where TUIControl: Control, ICommonCommandSupport, new()
{
  // ...
}

     SimpleEditorPane 实现了自定义编辑器需要的所有的关键的接口。继承了WindowPane,我们就实现了IVsWindowPane接口;实现了IOleCommandTarget 接口,就可以处理命令;IVsPersistDocDataIPersistFileFormat 用于持久化。TFactory是实现了IVsEditorFactory 的类,TUIControl 是实现了ICommonCommandSupport 接口的控件。

 

小结

     本篇文章我们开始创建一个自定义编辑器。首先我们了解了VS编辑器的基本架构。编辑器包含文档数据和文档视图,一个编辑器可以有多个视图,所有的视图都为同一个数据工作。

     VS IDE用Running Document Table来管理打开的文档,编辑器是有优先级的。

     本篇文章只是BlogItemEditor 示例的开篇,下一篇我们将从编辑器工厂类开始。

 

源码下载:

https://files.cnblogs.com/default/LearnVSXNow-8528.zip

 

原文链接:

http://dotneteers.net/blogs/divedeeper/archive/2008/03/12/LearnVSXNowPart15.aspx

posted @ 2010-07-17 14:08  明年我18  阅读(3429)  评论(1编辑  收藏  举报