Friday, June 30, 2006

 

Some useful DotNet tools

1.
http://www.jetbrains.com/products.html

公司推出的

Resharper http://www.jetbrains.com/resharper/
The Simple And Super Fast Profiler For .NET http://www.jetbrains.com/profiler/

2.
http://www.aisto.com/roeder/dotnet/

尤其是 Reflector for .NET

3.
n个VB.Net C#代码转换工具
from http://www.cnblogs.com/blaze/archive/2004/10/22/55426.html

1.http://www.kamalpatel.net/(最常用的,不过对于16进制的Int不能正常转换)
在线版:VB.Net => C# C#=>VB.Net
离线版:C#=>VB.Net
2.http://csharpconverter.claritycon.com/(推荐!非常好用的一个,几乎没出过太大的问题。)
在线版:C#=>VB.Net
离线版:C#=>VB.Net
3.http://www.ragingsmurf.com/
在线版:C#=>VB.Net
4.http://aspalliance.com/
在线版:C#=>VB.Net
5.http://developerfusion.com/
在线版:VB.Net => C# C#=>VB.Net
大家如果有其它的欢迎评论告诉我一声
最后送一个最管用的:呵呵,看了就知道.
http://www.4guysfromrolla.com/webtech/012702-1.shtml

Thursday, June 15, 2006

 

Some notes on SharpDevelop - 2

1.
from http://www.cnblogs.com/michael-zhang/articles/621148.html
http://www.cnblogs.com/michael-zhang/articles/629724.html
http://www.cnblogs.com/michael-zhang/articles/636381.html
http://www.cnblogs.com/michael-zhang/articles/651825.html
http://www.cnblogs.com/michael-zhang/articles/655267.html

SharpDevelop浅析_1_AddInTree

使用ICSharpCode.Core创建插件支持的应用程序

2、使用AddIn好处
方便扩展,可以看到SharpDevelop几乎是通过插接功能模块组装而成;核心可以不必实现自己的定义,方便地通过接口扩充功能;插件dll可以放在任意位置,对插件使用拷贝、粘贴式的部署。
许多应用程序也使用了一些插件机制,但大多数局限于特定的功能,如扩展菜单或新文件格式。SharpDevelop插件体系的目标是为应用程序提供简单易用而又强大的扩展点,allowing AddIns to extend nearly everything.

3、AddIn实现分析:
简单分析后,实现思路是这样的:定义一个接口ICommand,声明void DoCommand()方法,新增插件必须实现此接口;
单击菜单项或工具栏按钮时需要与主窗体交互,这可以通过在ICommand中定义属性MainForm或在void DoCommand(MainForm frm)中增加方法参数来传递主窗体的引用,这些实现起来倒也简单。
接下来的问题是如何通知应用程序新增加了插件呢,答案是使用xml配置文件,怎么组织这个配置文件的结构呢?这个问题其实成了实现插件功能的重点和难点,配置文件中希望说明新增插件的dll位置、类名、插接入主程序的菜单还是工具栏项、插接位置,或许还希望配置文件更容易被扩展?
这里(http: //www.codeproject.com/cs/library/Net_AddinProjFrmwork.asp)有一个结构不太好的配置文件定义形式(可能也是我们简单分析后会想到的定义方式,可以看出结构固定,且不易扩展),大家可以自行分析下:
sample.xml
1
2 1
3 Report Addin
4 1
5
6 Bar Code
7 1
8
9 Bar Code
10 1
11
12 Test Menu
13 AddinFunctionName
14 Some Status bar text
15 Some tool tip text
16 Addin2Settings.ico
17 Ctrl + H
18

19
20 ..
21 ..
22

23

24
25 ..
26 ..
27

28

29
30
31
32

33

现在来看看SharpDevelop的AddIn配置文件结构(参见Demo中的Entry.myAddins.Menus.addin):
Menus.addin
1 2 author = "michael zhang"
3 url = "http://www.cnblogs.com/michael-zhang/"
4 description = "基本插件"
5 addInManagerHidden = "true">
6
7
8

9
10
11

12
13
14
15

16
17

18
19 20 type = "Menu"
21 label = "${res:Demo.Menu.File}">
22 23 label = "Cmd&Black"
24 shortcut = "Control|B"
25 icon = "qq.face1"
26 class = "MainForm.CmdBlack"/>
27
28
29 30 label = "E&xit"
31 shortcut = "Control|X"
32 class = "MainForm.CmdExit"/>
33

34 35 type = "Menu"
36 label = "&Manager">
37
38

39

40
41 42 tooltip = "Black command"
43 icon = "qq.face1"
44 class = "MainForm.CmdBlack"/>
45
46 47 tooltip = "Exit the app"
48 icon = "CloseIcon"
49 class = "MainForm.CmdExit"/>
50

51
节提供了插件的名称、作者、url、插件描述等信息
提供了插件的唯一标识名称及版本,其它插件的配置文件可以引用该名称,如
指向该插件引用的dll位置
后面的形如...的是定义配置文件的核心数据,path节的name属性指明该节下的节点所处(在AddInTree中)的命名层次,节点下的MenuItem, ToolBarItem, FileFilter, Include等统称为Condon,各代表菜单项、工具栏按钮、文件过滤等,这些数据结构可以非常简单地被扩展、解析。
注:
addin配置文件中的 label = "${res:Demo.Menu.File}", icon = "qq.face1" 等属性值是指向资源文件的引用,资源文件见Entry项目的StrImgRes.resx

4、SharpDevelop插件树中的重要概念
Condon: 代表节下的一个(一般化)节点(如:...)统称为 Condon,该类含ID、Name、InsertBefore、InsertAfter、Conditions、Properties(类似于 HashTable的结构)等属性,配置节中的其它属性(除ID,Name,InsertBefore,InsertAfter外,如label, shortcut等)存储在Properties对象中。
Doozer: 代表Condon节点的更具体的实例,如MenuItemDoozer, ToolBarItemDoozer, FileFilterDoozer, IncludeDoozer, FileFilterDoozer等,用以创建具体的object对象,可以扩展编写自定义的Doozer。

5、Demo项目代码分析
至此,我们大概能猜到SharpDevelop中的插件机制是怎样的,下面就结合Demo的分析来体验一下SharpDevelop的插件功能:
MainForm.FrmMain.cs中使用单件模式获取此类,关键代码如下:
FrmMain.cs
1using ICSharpCode.Core;
2
3// 变量声明
4const string _BoundProperty = "FormBounds";
5Label _lblMsg;
6MenuStrip _menuStrip;
7ToolStrip _toolStrip;
8//
9void IniFrm()
10{
11 // 设置窗体位置
12 Rectangle rect = PropertyService.Get(_BoundProperty, new Rectangle(10, 10, this.Width, this.Height));
13 this.StartPosition = FormStartPosition.Manual;
14 this.Bounds = rect;
15 this.FormClosing += delegate
16 {
17 // 设置用户属性信息
18 PropertyService.Set(_BoundProperty, this.Bounds);
19 };
20
21 _lblMsg = new Label();
22 _lblMsg.Dock = DockStyle.Fill;
23 _lblMsg.Font = new Font("Arial", 16, FontStyle.Bold);
24 _lblMsg.Text = "App loaded!";
25 this.Controls.Add(_lblMsg);
26
27 _toolStrip = ToolbarService.CreateToolStrip(this, "/michael/myToolbar");
28 this.Controls.Add(_toolStrip);
29
30 _menuStrip = new MenuStrip();
31 MenuService.AddItemsToMenu(_menuStrip.Items, this, "/michael/myMenus");
32 this.Controls.Add(_menuStrip);
33}
34public void DrawMsg(string msg,Color color)
35{
36 _lblMsg.Text = msg;
37 _lblMsg.ForeColor = color;
38}主窗体的类中声明了Label, MenuStrip, ToolStrip 分别用以显示文字、菜单、工具栏。菜单、工具栏对象的获取通过ICSharpCode.Core内置类仅用简单的两行代码实现。此类中公开的 DrawMsg(...)方法用以向扩展菜单、工具栏按钮等提供公开可调用的功能。创建菜单、工具栏的两个方法中的一个重要参数是路径参数(分别是 "/michael/myToolbar"和"/michael/myMenus"),在前面的配置文件代码中可以找到相关定义节,其中相关节点一个重要属性是class, 该属性指定了(菜单、工具栏按钮的)相关类,其实现代码如下(MainForm项目的Commands.cs):
注:
FrmMain 类的IniFrm()方法中前几行代码用以设置启动窗体的大小和位置,使用到了ICSharpCode.Core.PropertyService类,该类将配置文件保存在"C:\Documents and Settings\michael\Application Data\michael's add-in test\myCfgParas.xml",其中[michael]是计算机名;[michael's add-in test]是应用程序名称,在程序启动时创建CoreStartup实例时指定;[myCfgParas.xml]由属性 CoreStartup.PropertiesName指定(详见后面Main()中的代码)。
Commands.cs
1//using
2namespace MainForm
3{
4 public class CmdBlack : AbstractMenuCommand
5 {
6 public override void Run()
7 {
8 FrmMain frm = (FrmMain)this.Owner;
9
10 StringBuilder sBuilder = new StringBuilder();
11 ArrayList alDatas = AddInTree.BuildItems("/michael/BlackText", null, true);
12 foreach (string str in alDatas)
13 sBuilder.AppendLine("suported types: " + str);
14
15 frm.DrawMsg(sBuilder.ToString(), Color.Black);
16 }
17 }
18
19 public class CmdExit : AbstractMenuCommand
20 {
21 public override void Run()
22 {
23 FrmMain frm = (FrmMain)this.Owner;
24 if (MessageBox.Show("Sure to exit?","Info:",MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question) == DialogResult.Yes)
25 frm.Close();
26 }
27 }
28}
可以看到插件类必须实现ICommand接口或继承AbstractMenuCommand类,ICommand接口定义的Owner属性返回该对象的拥有者Object,在此例子中即FrmMain对象,CmdBlack类中通过((FrmMain)Owner).DrawMsg(...)向主窗体发出功能命令。
至此,只剩下对ICSharpCode.Core进行必要的初始化配置了(参见Demo中的Entry项目Program.cs中的static void Main()函数):
void Main
1LoggingService.Info("Application start");
2Assembly asm = Assembly.GetExecutingAssembly();
3FileUtility.ApplicationRootPath = Path.GetDirectoryName(asm.Location);
4ResourceService.RegisterNeutralStrings(new ResourceManager("Entry.StrImgRes", asm));
5ResourceService.RegisterNeutralImages(new ResourceManager("Entry.StrImgRes", asm));
6
7LoggingService.Info("Starting core services");
8CoreStartup coreStartup = new CoreStartup("michael's add-in test");
9coreStartup.PropertiesName = "myCfgParas";
10coreStartup.StartCoreServices();
11// 在指定文件夹中搜寻插件的配置文件
12coreStartup.AddAddInsFromDirectory(Path.Combine(FileUtility.ApplicationRootPath, "myAddIns"));
13// AddinManager 插件的属性:保存用户禁用的插件信息
14coreStartup.ConfigureExternalAddIns(Path.Combine(PropertyService.ConfigDirectory, "AddIns.xml"));
15// AddinManager 插件的属性:保存用户安装的插件信息
16coreStartup.ConfigureUserAddIns(Path.Combine(PropertyService.ConfigDirectory, "AddInInstallTemp"),
17 Path.Combine(PropertyService.ConfigDirectory, "AddIns"));
18coreStartup.RunInitialization();
19try
20{
21 LoggingService.Info("Running application");
22 Application.Run(MainForm.FrmMain.Instance);
23}catch{
24
注:
Demo 项目引用的AddinManager也是SharpCode.Core中的一个插件实现,查看AddinManager.Addin,可见其定义了菜单项、新界面(点击新菜单时Run()方法中定义弹出的新窗体)、新界面的上下文菜单,上下文菜单中使用配置菜单项何时可用……
Demo项目中扩展菜单的例子参见Demo中的ExtenalMenus工程中的Command.cs和ExternalMenu.addin,该项目实现了两个新的菜单项,并且实现了一个自定义的Doozer。

6、总结:
a, ICSharpCode.Core默认实现的Doozer
Class 根据配置文件的声明由System.Reflection创建出相关对象
FileFilter 创建出路径后缀名的过滤选项提供给OpenFileDialog或是SaveFileDialog使用
Include 向addin tree引进一个(使用item属性)或多个(使用path属性)子项
Icon 用以创建文件类型与图标间的关联
MenuItem 创建菜单项 type可为:Seperator, CheckBox, Item/Command, Menu, Builder
ToolBarItem 创建工具栏按钮荐 type可为:Seperator, CheckBox, Item, ComboBox, DropDownButton

b, 配置文件中引用预定义资源格式
${res:ResourceName} 引用ResourceService系统资源
${property:PropertyName} 引用PropertyService中的属性
${env:VariableName} 引用系统变量
${exe:ProperName} 引用整个程序集的属性

c, Condition
[略(有待进一步分析)]

7、相关资料:
《Dissecting a C# Application Inside SharpDevelop.pdf》
SharpDevelop源代码(\src\ 和 \samples\ICSharpCode.Core.Demo\)
http://www.sharpdevelop.com/OpenSource/SD/Default.aspx
http://www.codeproject.com/csharp/ICSharpCodeCore.asp

SharpDevelop浅析_2_User Interface

创建易扩展且功能模块松散耦合的应用程序

2、相关概念:
主窗体部分: Workbench
磁盘文件查看窗口: Pad
文件查看窗口: ViewContent
说明:
Pad可以使用ICSharpCode.Core的Addin插件机制来扩充Pad,如Visual Studio的资源管理器、类查看器、属性窗口、消息窗口等都属于Pad
ViewContent同样支持插件扩充,根据不同的文件类型使用不同的控件来处理显示,Demo中已实现的文件类型支持:网页文件、图片、普通文本,Visual Studio中的代码窗口、窗体设计器、资源编辑器、对象查看器等都属于ViewContent
Workbench与WorkbenchLayout结合使用来控制主窗口的外观显示,如普通的MDI(PhotoShop?)或Demo使用的WeifenLuo.WinFormsUI.Docking显示方式……

3、Demo代码分析
(注:下面的分析前提是你已经了解如何使用ICSharpCode.Core的Addin插件机制)
通过上面的界面及功能说明,可以确定程序至少应包括接口声明、基本实现两个项目,而接口中又分为:Pad接口、ViewContent接口、Workbench接口,这样的程序设计重点与难点在哪呢?思考后的结果应该是接口,从接口中能看到程序各部分是如何交互的,以及插件的扩展点,接口的设计好坏决定着程序的易维护性与易扩展性,所以下面就重点看接口的定义(附介绍相关对象的创建等):

Pad接口
1// Core|Interface 定义部分:
2public interface IPadContent : IDisposable
3{
4 System.Windows.Forms.Control Control { get; }
5
6 void RedrawContent();
7}
8public class PadDescriptor : IDisposable
9{
10 Codon codon;
11 IPadContent padContent;
12 bool padContentCreated;
13
14 public string Title {
15 get {
16 return codon.Properties["title"];
17 }
18 }
19 public string Icon {
20 get {
21 return codon.Properties["icon"];
22 }
23 }
24 public string Class {
25 get {
26 return codon.Properties["class"];
27 }
28 }
29 public IPadContent PadContent {
30 get {
31 CreatePad();
32 return padContent;
33 }
34 }
35 public void CreatePad()
36 {
37 if (!padContentCreated) {
38 padContentCreated = true;
39 padContent = (IPadContent)codon.AddIn.CreateObject(Class);
40 }
41 }
42 //省略部分方法、属性
43 public PadDescriptor(Codon codon)
44 {
45 this.codon = codon;
46 }
47}
48// Gui项目中的实现
49class PadContentWrapper : DockContent
50{
51 PadDescriptor padDescriptor; //通过此对象来获取相关属性
52 //
53}看到IPadContent接口返回一个WinForm的Control控件,此控件在实现时被相应的窗体获取并适当地显示,Demo中的磁盘查看器Pad返回的Control是一个UserConrol,使用了TreeView和ListView组合。注意IPadContent接口中定义返回一个Control而非Form是很聪明的技巧,可以看到实现端的PadContentWrapper继承自WeifenLuo.WinFormsUI.DockContent,这依赖于实现端的表现方式,而接口可以不受影响。PadDescriptor是个辅助类,用以返回Pad相关的属性信息,包括返回IPadContent的实例对象。
接下来看Pad的xml声明及客户端调用:
Pad声明及使用
1//取自Entry项目的SD_UI.addin
2
3
16 17 category = "Tools"
18 title = "${res:MainWindow.Windows.FileScoutLabel}"
19 icon = "PadIcons.FileBrowser"
20 shortcut = "Control|Alt|F"
21 class = "SDUserInterface.GUI.Pad.FileScout"/>
22

23// 取自Gui项目的DefaultWorkbench.cs
24void InitializeWorkspace()
25{
26 //
27 ArrayList contents = AddInTree.GetTreeNode("/SharpDevelop/Workbench/Pads").BuildChildItems(this);
28 foreach (PadDescriptor content in contents)
29 {
30 if (content != null)
31 {
32 ShowPad(content);
33 }
34 }
35 //
36}
配置文件中的class指定了实现IPadContent的一个类型(全称限定名),使用时通过ICSharpCode.Core的AddInTree对象构建相关PadDescriptor集合……

下面来看ViewContent的定义:

ViewContent接口
1// Core|Interface 定义部分:
2public interface IViewContent : IDisposable
3{
4 Control Control { get; set; }
5
6 IWorkbenchWindow WorkbenchWindow { get; set; }
7
8 string TitleName { get; set; }
9
10 string FileName { get; set; }
11
12 bool IsReadOnly { get; }
13
14 void Load(string fileName);
15
16 event EventHandler TitleNameChanged;
17}
18public class DisplayBindingDescriptor
19{
20 object binding = null;
21 Codon codon;
22
23 public IDisplayBinding Binding {
24 get {
25 if (binding == null) {
26 binding = codon.AddIn.CreateObject(codon.Properties["class"]);
27 }
28 return binding as IDisplayBinding;
29 }
30 }
31
32 public Codon Codon {
33 get {
34 return codon;
35 }
36 }
37
38 public DisplayBindingDescriptor(Codon codon)
39 {
40 this.codon = codon;
41 }
42
43 public bool CanAttachToFile(string fileName)
44 {
45 string fileNameRegex = codon.Properties["fileNamePattern"];
46 if (fileNameRegex == null || fileNameRegex.Length == 0) // no regex specified
47 return true;
48 return Regex.IsMatch(fileName, fileNameRegex, RegexOptions.IgnoreCase);
49 }
50}
51public interface IDisplayBinding
52{
53 bool CanCreateContentForFile(string fileName);
54
55 IViewContent CreateContentForFile(string fileName);
56}
57// Gui项目中的实现:
58public class SdiWorkspaceWindow : DockContent, IWorkbenchWindow
59{
60 IViewContent content;
61 //
62}
可以看到IViewContent同样是返回一个Conrol,供使用端(SdiWorkspaceWindow)根据需要封装组合;DisplayBindingDescriptor同样是个辅助类,除了返回ViewContent的相关属性信息外,提供了bool CanAttachToFile(string fileName)方法,用以判断当前显示插件是否可以显示相关类型的文件,这里的判断是通过配置文件中的fileNamePattern属性作正则判断(注:文件名称符合一定规则的可能会用到此属性,一般不常用),注意到该辅助类返回了一个IDisplayBinding接口,查看该接口的方法可以看到使用它来更进一步地判断当前文件是否是可支持类型(通过文件扩展名或试读取等方式),如果属于该插件支持类型的文件则创建并返回IViewContenet接口。ViewContent插件的声明如下:

ViewContent插件声明
1
2 3 supportedformats = "Web Pages"
4 class = "SDUserInterface.GUI.ViewContent.BrowserDisplayBinding"/>
5 6 insertafter = "Browser"
7 supportedformats = "Text Files,Source Files"
8 class = "SDUserInterface.GUI.ViewContent.TextViewDisplayBinding" />
9 10 insertbefore = "Text"
11 supportedformats = "图片"
12 class = "SDUserInterface.GUI.ViewContent.ImageDisplayBinding" />
13

值得注意的是insertbefore, insertafter 属性,此属性指明获取所有DisplayBindingDescriptor后的先后顺序,如:一个.rtf文件可以由Office-Word和记事本打开,一般要优先选择使用Word打开。
获取ViewContent对象的过程如下:双击磁盘文件Pad中的一个文件项时,调用FileService中的OpenFile(string fileName)方法,相关代码如下:

获取/使用ViewContent
1// 取自Gui项目中的Common/FileService.cs
2public static IWorkbenchWindow OpenFile(string fileName)
3{
4 //
5 IDisplayBinding binding = DisplayBindingService.GetBindingPerFileName(fileName);
6
7 if (binding != null) {
8 binding.CreateContentForFile(fileName);
9 WorkbenchSingleton.Workbench.ShowView(newContent);
10 //
11 } else {
12 throw new ApplicationException("Can't open " + fileName + ", no display codon found.");
13 }
14 return GetOpenFile(fileName);
15}
16// 取自Gui项目中的Common/DisplayBindingService.cs
17static DisplayBindingDescriptor GetCodonPerFileName(string filename)
18{
19 foreach (DisplayBindingDescriptor binding in bindings) {
20 if (binding.CanAttachToFile(filename)) {
21 if (binding.Binding != null && binding.Binding.CanCreateContentForFile(filename)) {
22 return binding;
23 }
24 }
25 }
26 return null;
27}
接下来看主窗体的定义:

Workbench接口
1public interface IWorkbench
2{
3 string Title { get; set; }
4
5 List ViewContentCollection { get; }
6
7 List PadContentCollection { get; }
8
9 IWorkbenchWindow ActiveWorkbenchWindow { get; }
10
11 object ActiveContent { get; }
12
13 IWorkbenchLayout WorkbenchLayout { get; set; }
14
15 void ShowView(IViewContent content);
16
17 void CloseAllViews();
18
19 void CloseView(IViewContent content);
20
21 void ShowPad(PadDescriptor content);
22
23 PadDescriptor GetPad(Type type);
24
25 void RedrawAllComponents();
26}
27public interface IWorkbenchLayout
28{
29 bool FullScreen { get; set; }
30
31 IWorkbenchWindow ActiveWorkbenchwindow { get; }
32
33 object ActiveContent { get; }
34
35 void Attach(IWorkbench workbench);
36
37 void Detach();
38
39 void ShowPad(PadDescriptor content);
40
41 void ShowPad(PadDescriptor content,bool bActivateIt);
42
43 IWorkbenchWindow ShowView(IViewContent content);
44
45 void RedrawAllComponents();
46
47 void LoadConfiguration();
48 void StoreConfiguration();
49}
IWorkbench定义主窗体,IWorkbenchLayout定义窗体布局,可以看到IWorkbench的两个重要属性是Pad和ViewContent的对象集合(维护已打开的窗体记录,避免重复打开等作用),其实现类的ShowPad()/ShowView()方法执行的操作即向对应的集合添加成员,然后调用IWorkbenchLayout的ShowPad()/ShowView()。IWorkbenchLayout的LoadConfiguration()和StoreConfiguration()方法用以在窗体加载或关闭时执行加载或保存子窗口布局的操作。

至此Demo的核心已分析完了,我们可以根据定义的接口扩展程序,如增加项目管理/查看Pad, 增加.swf等文件查看的ViewContent, 或更换主界面显示为普通MDI方式……

4、总结:
此Demo更说明了ICSharpCode.Core的插件支持可以应用到很多的方面:
a, 界面组成部分Pad
b, 界面中不同类型的文件查看器ViewContent
c, 磁盘文件查看Pad的下侧文件列表针对不同文件显示相关图标(详见SD_UI.addin文件的"/Workspace/Icons"层次下的定义)
...
SharpDevelop的这种界面设计方案使得应用程序很容易扩展和进一步开发,而不与已开发的模块冲突;从中得到的启发是应用程序要重点设计接口(契约)以及处理对象间的关系……

说明:Demo代码基本来源于SharpDevelop源码,删改了部分代码以使Demo精简和突出重点。


SharpDevelop浅析_3_Internationalization & TextEditor

国际化、文档编辑器、语法高亮显示

2、SharpDevelop的Internationalization的使用
多语言的实现就是在显示时根据键获取相应语言环境下的键值(Dictionary),因此在编写程序时应该引用键,而非直接书写要显示的字符。一般地,应用程序的显示包括菜单、状态栏、提示字符等,因此Demo中的主菜单在配置文件(参见Basic.addin)中使用label = "${res:Menu.File.Open}",退出应用程序的提示字符使用string s = StringParser.Parse("${res:Info.Exit}");。ICSharpCode.Core.dll支持语言环境的实时修改(修改后语言设置不需重启应用程序),因此要在应用程序订阅语言环境改变的事件,相关代码如下:


语言环境的修改及事件响应
1//a, 设置语言
2//引自SharpPad项目的Dialogs\SelectCulturePanel.cs
3public override bool ReceiveDialogMessage(DialogMessage message)
4{
5 if (message == DialogMessage.OK) {
6 if (SelectedCulture != null) {
7 PropertyService.Set("CoreProperties.UILanguage", SelectedCulture);
8 }
9 }
10 return true;
11}
12//返回当前窗体用户选择的语言设置(从显示国家国旗的ListView控件)
13string SelectedCulture {
14 get {
15 if (listView.SelectedItems.Count > 0) {
16 return listView.SelectedItems[0].SubItems[1].Text;
17 }
18 return null;
19 }
20}
21//b, 刷新当前应用程序
22//引自SharpPad项目的SharpPad.cs
23void IniFrm()
24{
25 //
26 _menuStrip = new MenuStrip();
27 MenuService.AddItemsToMenu(_menuStrip.Items, this, "/michael/myMenus");
28 this.Controls.Add(_menuStrip);
29
30 PropertyService.PropertyChanged += new PropertyChangedEventHandler(PropertyService_PropertyChanged);
31 ResourceService.LanguageChanged += delegate{
32 //更新菜单项的Text显示
33 foreach (ToolStripItem item in _menuStrip.Items)
34 {
35 if (item is IStatusUpdate)
36 {
37 ((IStatusUpdate)item).UpdateText();
38 }
39 }
40 };
41}
42//引自ICSharpCode.Core的src\AddinTree\Addin\DefaultDoozers\MenuItem\Gui\IStatusUpdate.Core
43using System;
44namespace ICSharpCode.Core
45{
46 public interface IStatusUpdate
47 {
48 void UpdateStatus();
49 void UpdateText();
50 }
51}



另外,可以看到Demo中的选项命令窗口也采用了插件模式来构造窗体(实现细节就不多谈了),容器窗体是TreeViewOption,插件窗体如:SelectCulturePanel, SelectStylePanel, DemoNothing,真是“扩展--无处不在”呀。SharpDevelop源码中的这些窗体的成员控件是通过.xfrm配置文件配置,窗体继承自XmlUserControl来根据配置文件生成控件,有一定的灵活性。
新的语言包资源文件放在\data\resources\目录下(如StringResources.cn-gb.resources),注意应用程序默认语言选项以及在未找到相关语言资源时都是引用Entry中的myRes.resx资源文件;语言声明文件是\data\resources\languages\LanguageDefinition.xml,其格式如下:
LanguageDefinition.xml
1
2
3
4
5

SharpPad中动态显示可选语言项的分析类是LanguageService.cs和Language.cs,此处就不多解释了。

3、SharpDevelop的Internationalization的实现分析
SharpDevelop的多语言支持的键值对是通过本地资源文件存储的,当Demo中更改语言环境设置时,引发ICSharpCode.Core.dll中的事件及方法顺序如下:
PropertyService类的属性更新引发PropertyChanged事件 -> ResourceService响应接收到的事件并重新加载保存在内存中的资源键值对,然后引发LanguageChanged事件(由使用端接收并作相关处理,如Demo中的SharpPad.cs中的事件订阅/处理)属性更新的相关代码如下:
属性更改
1//PropertyService类实际是提供了对Properties类的包装,因此直接看Properties类中的代码:
2//引自ICSharpCode.Core\src\Services\PropertService\Properties.cs
3public void Set(string property, T value)
4{
5 T oldValue = default(T);
6 if (!properties.ContainsKey(property)) {
7 properties.Add(property, value);
8 } else {
9 oldValue = Get(property, value);
10 properties[property] = value;
11 }
12 OnPropertyChanged(new PropertyChangedEventArgs(this, property, oldValue, value));
13}
14protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
15{
16 if (PropertyChanged != null) {
17 PropertyChanged(this, e);
18 }
19}
20public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
21public event PropertyChangedEventHandler PropertyChanged;
资源服务类的事件响应代码如下:
ResourceService类的事件响应
1//引自ICSharpCode.Core\src\Services\ResourceService\ResourceService.cs
2//首先在类的初始化中订阅属性更改事件:
3PropertyService.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChange);
4//类级别变量:
5static Hashtable localStrings = null;
6static Hashtable localIcons = null;
7public static event EventHandler LanguageChanged;
8//事件响应:
9static void OnPropertyChange(object sender, PropertyChangedEventArgs e)
10{
11 if (e.Key == uiLanguageProperty && e.NewValue != e.OldValue) {
12 LoadLanguageResources((string)e.NewValue);
13 if (LanguageChanged != null)
14 LanguageChanged(null, e);
15 }
16}
17static void LoadLanguageResources(string language)
18{
19 try
20 {
21 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language);
22 }
23 catch (Exception)
24 {
25 try
26 {
27 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(language.Split('-')[0]);
28 }
29 catch (Exception) { }
30 }
31
32 localStrings = Load(stringResources, language);
33 if (localStrings == null && language.IndexOf('-') > 0)
34 {
35 localStrings = Load(stringResources, language.Split('-')[0]);
36 }
37
38 localIcons = Load(imageResources, language);
39 if (localIcons == null && language.IndexOf('-') > 0)
40 {
41 localIcons = Load(imageResources, language.Split('-')[0]);
42 }
43
44 localStringsResMgrs.Clear();
45 localIconsResMgrs.Clear();
46 currentLanguage = language;
47 foreach (ResourceAssembly ra in resourceAssemblies)
48 {
49 ra.Load();
50 }
51}
52static Hashtable Load(string name, string language)
53{
54 return Load(resourceDirectory + Path.DirectorySeparatorChar + name + "." + language + ".resources");
55}
56static Hashtable Load(string fileName)
57{
58 if (File.Exists(fileName)) {
59 Hashtable resources = new Hashtable();
60 ResourceReader rr = new ResourceReader(fileName);
61 foreach (DictionaryEntry entry in rr) {
62 resources.Add(entry.Key, entry.Value);
63 }
64 rr.Close();
65 return resources;
66 }
67 return null;
68}

4、SharpDevelop的文档管理的基本概念
代码编写工具的核心是代码编辑窗口(如打开一个.cs文件的窗口),如何在应用程序中存储该窗口内的字符内容?有些文件可能有上千行,统计下来可能有过万个字符,如果使用string对象存储字符内容,显然是不能满足性能要求,而且要实现语法高亮显示的话,还需要区分存储TextWord和相应颜色……
先看一下字符管理的基本要求:
ITextBufferStrategy接口
1//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\ITextBufferStrategy.cs
2namespace ICSharpCode.TextEditor.Document
3{
4 /**////
5 /// Interface to describe a sequence of characters that can be edited.
6 ///

7 public interface ITextBufferStrategy
8 {
9 /**////
10 /// The current length of the sequence of characters that can be edited.
11 ///

12 int Length {
13 get;
14 }
15
16 /**////
17 /// Inserts a string of characters into the sequence.
18 ///

19 ///
20 /// offset where to insert the string.
21 ///
22 ///
23 /// text to be inserted.
24 ///
25 void Insert(int offset, string text);
26
27 /**////
28 /// Removes some portion of the sequence.
29 ///

30 ///
31 /// offset of the remove.
32 ///
33 ///
34 /// number of characters to remove.
35 ///
36 void Remove(int offset, int length);
37
38 /**////
39 /// Replace some portion of the sequence.
40 ///

41 ///
42 /// offset.
43 ///
44 ///
45 /// number of characters to replace.
46 ///
47 ///
48 /// text to be replaced with.
49 ///
50 void Replace(int offset, int length, string text);
51
52 /**////
53 /// Fetches a string of characters contained in the sequence.
54 ///

55 ///
56 /// Offset into the sequence to fetch
57 ///
58 ///
59 /// number of characters to copy.
60 ///
61 string GetText(int offset, int length);
62
63 /**////
64 /// Returns a specific char of the sequence.
65 ///

66 ///
67 /// Offset of the char to get.
68 ///
69 char GetCharAt(int offset);
70
71 /**////
72 /// This method sets the stored content.
73 ///

74 ///
75 /// The string that represents the character sequence.
76 ///
77 void SetContent(string text);
78 }
79}
SharpDevelop采用的策略是使用带Gap的字符,Gap的长度小于预定规格时,按约定增加一定长度(有些类似于SqlServer的表空间管理?),插入/修改/删除字符时只是修改Gap的长度和位置和移动部分字符,核心函数如下:
GapTextBufferStrategy类
1//引自ICSharpCode.TextEditor\src\Document\TextBufferStrategy\GapTextBufferStrategy.cs
2//类级别变量:
3char[] buffer = new char[0];
4int gapBeginOffset = 0;
5int gapEndOffset = 0;
6/**////
7/// 小于此长度时增长Buffer/Gap
8///

9int minGapLength = 32;
10/**////
11/// 每次需要增长时,增长此长度的Gap
12///

13int maxGapLength = 256;
14//重要函数:
15public void SetContent(string text)
16{
17 if (text == null) {
18 text = String.Empty;
19 }
20 buffer = text.ToCharArray();
21 gapBeginOffset = gapEndOffset = 0;
22}
23public void Insert(int offset, string text)
24{
25 Replace(offset, 0, text);
26}
27public void Remove(int offset, int length)
28{
29 Replace(offset, length, String.Empty);
30}
31public void Replace(int offset, int length, string text)
32{
33 if (text == null) {
34 text = String.Empty;
35 }
36
37 // Math.Max is used so that if we need to resize the array
38 // the new array has enough space for all old chars
39 PlaceGap(offset + length, Math.Max(text.Length - length, 0));
40 text.CopyTo(0, buffer, offset, text.Length);
41 gapBeginOffset += text.Length - length;
42}
43void PlaceGap(int offset, int length)
44{
45 int deltaLength = GapLength - length;
46 // 如果Gap的长度足够大,则只是作移动相关字符的处理
47 if (minGapLength <= deltaLength && deltaLength <= maxGapLength) {
48 int delta = gapBeginOffset - offset;
49 // check if the gap is already in place
50 if (offset == gapBeginOffset) {
51 return;
52 } else if (offset < gapBeginOffset) {
53 int gapLength = gapEndOffset - gapBeginOffset;
54 Array.Copy(buffer, offset, buffer, offset + gapLength, delta);
55 } else { //offset > gapBeginOffset
56 Array.Copy(buffer, gapEndOffset, buffer, gapBeginOffset, -delta);
57 }
58 gapBeginOffset -= delta;
59 gapEndOffset -= delta;
60 return;
61 }
62
63 // 否则,需要新分析buffer的大小,并修改offset等位置数值
64 int oldLength = GapLength;
65 int newLength = maxGapLength + length;
66 int newGapEndOffset = offset + newLength;
67 char[] newBuffer = new char[buffer.Length + newLength - oldLength]; //新分配后将有maxGapLength长度的Gap
68
69 if (oldLength == 0) {
70 Array.Copy(buffer, 0, newBuffer, 0, offset);
71 Array.Copy(buffer, offset, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
72 } else if (offset < gapBeginOffset) {
73 int delta = gapBeginOffset - offset;
74 Array.Copy(buffer, 0, newBuffer, 0, offset);
75 Array.Copy(buffer, offset, newBuffer, newGapEndOffset, delta);
76 Array.Copy(buffer, gapEndOffset, newBuffer, newGapEndOffset + delta, buffer.Length - gapEndOffset);
77 } else {
78 int delta = offset - gapBeginOffset;
79 Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
80 Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, delta);
81 Array.Copy(buffer, gapEndOffset + delta, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
82 }
83
84 buffer = newBuffer;
85 gapBeginOffset = offset;
86 gapEndOffset = newGapEndOffset;
87}

有了基本的数据容器,核心问题已经解决了,但是在绘制界面时直接使用上面的类,显然不够方便,于是就定义了LineSegment和TextWord类来分别存储行、单词,注意这两个类存储的只是int类型的offset和length信息,而不存储字符或字符串对象。注意TextWord类中有个HighlightColor类型的变量用以存储语法高亮显示信息。
有了上面的这些类,便可以组合起来补充些其它信息对外提供服务了,IDocument接口存在的目的即在于此,它封装了ITextEditorProperties(是否显示空格/Tab/Eol/HRuler等)、UndoStack、ITextBufferStrategy、IHighlightingStrategy、FoldingManager、BookmarkManager等属性对象。

5、SharpDevelop的SyntaxHighlighting配置文件的定义
是时候对语法高亮显示作一些分析了,此处不详述其实现,而重点分析其配置定义,从中亦可以猜出部分实现。
相关配置文件均保存在ICSharpCode.TextEditor项目\Resource\目录下
首先SyntaxMode.xml文件中显示了已定义的文件类型及其声明文件位置,其解析类参见\src\Document\HighlightingStrategy\SyntaxModes\FileSyntaxModeProvider.cs


SyntaxMode.xml
1
2 3 name = "ASP/XHTML"
4 extensions = ".asp;.aspx;.asax;.asmx"/>
5
6 7 name = "BAT"
8 extensions = ".bat"/>
9
10 11 name = "C++.NET"
12 extensions = ".c;.h;.cc;.C;.cpp;.hpp"/>
13
14 15 name = "C#"
16 extensions = ".cs"/>
17
18 19 name = "XML"
20 extensions = ".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd; .wxs;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets; .xaml;.xpt;.xft;.map;.wsdl;.disco"/>
21

取CSharp-Mode.xshd(注:xshd是Xml Syntax Highlighting Definition的缩写)为例,查看其定义:


CSharp-Mode.xshd
1
2
3
4
5

6
7
8
9 &<>~!%^*()-+=|\#/{}[]:;"' , .?
10
11 #
12

13
14 /*
15 */
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

42
43
44
45
46

47
48
49
50
51
52
53
54
55
56
57

58

59

60

上面的文件公摘选了部分标签,其中
标签定义了指定名称属性的指定值
标签定义了数值的字体显示样式
定义了分隔单词的字符
定义了包含在此指定Begin/End中的字符的显示样式,注意没有标签的一般设stopateol(stop at end-of-line)为true
指定了其子集声明的单词的显示样式

6、SharpDevelop的TextEditor控件的实现概述
接下来的任务是要显示和支持用户输入了,直接使用TextBox或RichTextBox好像都不太现实,效率上也必定有不少损失,于是SharpDevelop的方式是直接继承自Control, Panel, UserControl 的方式来实现编辑控件(参见ICSharpCode.TextEditor项目\src\Gui\...)。
首先使用TextEditorControlBase(继承自UserControl)封装当前文件路径、Encoding、IDocument对象、ITextEditorProperties对象、快捷键列表(Dictionary类型)的变量,提供LoadFile()、SaveFile()等重要方法。
TextEditorControl类继承自上面的类,并声明了Panel、Splitter、TextAreaControl、PrintDocument控件,其中显示文件的核心控件是TextAreaControl,该控件在此类中被声明了两个变量,默认只有一个primaryTextArea显示,支持切分为两个窗口的显示,当有两个窗口时,Splitter控件才有效;PrinDocument控件用以打印输出内容;该类的另一个重要功能是提供了UnDo()、Redo()方法。
TextAreaControl(继承自Panel)控件,封装了TextArea、VScrollBar、HScrollBar控件,其中TextArea负责文件数据显示,另外两个控件负责文件内容大于可见尺寸时的滚动条服务。
TextArea(继承自Control)封装了TextView, IconBarMargin, GutterMargin, FoldMargin, SelectionManager, Caret 等控件或类对象,其中前四个控件均继承自AbstractMargin,代表区域对象,各自负责字符区域(较大的文件内容绘制区)、图标区域(如Bookmark图标所在列)、Gutter区域(如行号)、折叠控制区域的绘制,SelectionManager用以控制选中项,Caret用以控制光标位置调整和显示。

下面是一些我在读代码的过程中有过的疑问及解答:
a, 加载文件时发生了什么?
答:加载文件时控件根据文件的后缀名选择了相应的高亮显示策略,然后读取文件的内容并生成相应的GapTextBufferStrategy, LineSegment, TextWord 等对象,并且对所有的TextWord对象的HighlightColor类型成员变量完成分析赋值(用以在Paint函数中显示),相关代码如下:


LoadFile()相关代码
1//取自TextEditorControlBase.cs
2public void LoadFile(string fileName, bool autoLoadHighlighting, bool autodetectEncoding)
3{
4 BeginUpdate();
5 document.TextContent = String.Empty;
6 document.UndoStack.ClearAll();
7 document.BookmarkManager.Clear();
8 if (autoLoadHighlighting) {
9 //根据文件扩展名判断并赋值高亮显示的策略
10 document.HighlightingStrategy = HighlightingStrategyFactory.CreateHighlightingStrategyForFile(fileName);
11 }
12
13 if (autodetectEncoding) {
14 Encoding encoding = this.Encoding;
15 //赋值
16 Document.TextContent = Util.FileReader.ReadFileContent(fileName, ref encoding, this.TextEditorProperties.Encoding);
17 this.Encoding = encoding;
18 } else {
19 using (StreamReader reader = new StreamReader(fileName, this.Encoding)) {
20 Document.TextContent = reader.ReadToEnd();
21 }
22 }
23
24 this.FileName = fileName;
25 OptionsChanged();
26 Document.UpdateQueue.Clear();
27 EndUpdate();
28
29 Refresh();
30}
31//引自DefaultDocument.cs
32ITextBufferStrategy textBufferStrategy = null;
33ILineManager lineTrackingStrategy = null;
34public string TextContent {
35 get {
36 return GetText(0, textBufferStrategy.Length);
37 }
38 set {
39 Debug.Assert(textBufferStrategy != null);
40 Debug.Assert(lineTrackingStrategy != null);
41 OnDocumentAboutToBeChanged(new DocumentEventArgs(this, 0, 0, value));
42 //赋值
43 textBufferStrategy.SetContent(value);
44 //赋值 && 分析并完成高亮分析的赋值
45 lineTrackingStrategy.SetContent(value);
46
47 OnDocumentChanged(new DocumentEventArgs(this, 0, 0, value));
48 OnTextContentChanged(EventArgs.Empty);
49 }
50}
51//引自DefaultLineManager.cs
52public void SetContent(string text)
53{
54 lineCollection.Clear();
55 if (text != null) {
56 textLength = text.Length;
57 // 生成LineSegment集合
58 CreateLines(text, 0, 0);
59 // 高亮分析
60 RunHighlighter();
61 }
62}
b, 控件如何响应键盘事件?
答:对于方向键及快捷功能键通过预定义的实现IEditAction接口的类响应(执行功能,不影响字符内容);其它字母/数字键直接输入,同时执行更新Folding, Bookmark, Higlighting等属性信息。相关代码:
键盘事件响应
1//功能键的定义(引自TextEditorControlBase.cs):
2protected Dictionary editactions = new Dictionary();
3protected TextEditorControlBase()
4{
5 GenerateDefaultActions();
6 HighlightingManager.Manager.ReloadSyntaxHighlighting += new EventHandler(ReloadHighlighting);
7}
8void GenerateDefaultActions()
9{
10 editactions[Keys.Left] = new CaretLeft();
11 editactions[Keys.Left | Keys.Shift] = new ShiftCaretLeft();
12 editactions[Keys.Left | Keys.Control] = new WordLeft();
13 editactions[Keys.Left | Keys.Control | Keys.Shift] = new ShiftWordLeft();
14 editactions[Keys.Right] = new CaretRight();
15 editactions[Keys.Right | Keys.Shift] = new ShiftCaretRight();
16 editactions[Keys.Right | Keys.Control] = new WordRight();
17 editactions[Keys.Right | Keys.Control | Keys.Shift] = new ShiftWordRight();
18 editactions[Keys.Up] = new CaretUp();
19 editactions[Keys.Up | Keys.Shift] = new ShiftCaretUp();
20 editactions[Keys.Up | Keys.Control] = new ScrollLineUp();
21 editactions[Keys.Down] = new CaretDown();
22 editactions[Keys.Down | Keys.Shift] = new ShiftCaretDown();
23 editactions[Keys.Down | Keys.Control] = new ScrollLineDown();
24
25 editactions[Keys.Insert] = new ToggleEditMode();
26 editactions[Keys.Insert | Keys.Control] = new Copy();
27 editactions[Keys.Insert | Keys.Shift] = new Paste();
28 editactions[Keys.Delete] = new Delete();
29 editactions[Keys.Delete | Keys.Shift] = new Cut();
30 editactions[Keys.Home] = new Home();
31 editactions[Keys.Home | Keys.Shift] = new ShiftHome();
32 editactions[Keys.Home | Keys.Control] = new MoveToStart();
33 editactions[Keys.Home | Keys.Control | Keys.Shift] = new ShiftMoveToStart();
34 editactions[Keys.End] = new End();
35 editactions[Keys.End | Keys.Shift] = new ShiftEnd();
36 editactions[Keys.End | Keys.Control] = new MoveToEnd();
37 editactions[Keys.End | Keys.Control | Keys.Shift] = new ShiftMoveToEnd();
38 editactions[Keys.PageUp] = new MovePageUp();
39 editactions[Keys.PageUp | Keys.Shift] = new ShiftMovePageUp();
40 editactions[Keys.PageDown] = new MovePageDown();
41 editactions[Keys.PageDown | Keys.Shift] = new ShiftMovePageDown();
42
43 editactions[Keys.Return] = new Return();
44 editactions[Keys.Tab] = new Tab();
45 editactions[Keys.Tab | Keys.Shift] = new ShiftTab();
46 editactions[Keys.Back] = new Backspace();
47 editactions[Keys.Back | Keys.Shift] = new Backspace();
48
49 editactions[Keys.X | Keys.Control] = new Cut();
50 editactions[Keys.C | Keys.Control] = new Copy();
51 editactions[Keys.V | Keys.Control] = new Paste();
52
53 editactions[Keys.A | Keys.Control] = new SelectWholeDocument();
54 editactions[Keys.Escape] = new ClearAllSelections();
55
56 editactions[Keys.Divide | Keys.Control] = new ToggleComment();
57 editactions[Keys.OemQuestion | Keys.Control] = new ToggleComment();
58
59 editactions[Keys.Back | Keys.Alt] = new Actions.Undo();
60 editactions[Keys.Z | Keys.Control] = new Actions.Undo();
61 editactions[Keys.Y | Keys.Control] = new Redo();
62
63 editactions[Keys.Delete | Keys.Control] = new DeleteWord();
64 editactions[Keys.Back | Keys.Control] = new WordBackspace();
65 editactions[Keys.D | Keys.Control] = new DeleteLine();
66 editactions[Keys.D | Keys.Shift | Keys.Control] = new DeleteToLineEnd();
67
68 editactions[Keys.B | Keys.Control] = new GotoMatchingBrace();
69}
70internal IEditAction GetEditAction(Keys keyData)
71{
72 if (!editactions.ContainsKey(keyData)) {
73 return null;
74 }
75 return (IEditAction)editactions[keyData];
76}
77//功能键的响应(取自TextArea.cs):
78protected override bool ProcessDialogKey(Keys keyData)
79{
80 return ExecuteDialogKey(keyData) || base.ProcessDialogKey(keyData);
81}
82public bool ExecuteDialogKey(Keys keyData)
83{
84 // try, if a dialog key processor was set to use this
85 if (DoProcessDialogKey != null && DoProcessDialogKey(keyData)) {
86 return true;
87 }
88
89 if (keyData == Keys.Back || keyData == Keys.Delete || keyData == Keys.Enter) {
90 if (TextEditorProperties.UseCustomLine == true) {
91 if (SelectionManager.HasSomethingSelected) {
92 if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
93 return true;
94 } else {
95 int curLineNr = Document.GetLineNumberForOffset(Caret.Offset);
96 if (Document.CustomLineManager.IsReadOnly(curLineNr, false) == true)
97 return true;
98 if ((Caret.Column == 0) && (curLineNr - 1 >= 0) && keyData == Keys.Back &&
99 Document.CustomLineManager.IsReadOnly(curLineNr - 1, false) == true)
100 return true;
101 if (keyData == Keys.Delete) {
102 LineSegment curLine = Document.GetLineSegment(curLineNr);
103 if (curLine.Offset + curLine.Length == Caret.Offset &&
104 Document.CustomLineManager.IsReadOnly(curLineNr + 1, false) == true) {
105 return true;
106 }
107 }
108 }
109 }
110 }
111
112 // if not (or the process was 'silent', use the standard edit actions
113 IEditAction action = motherTextEditorControl.GetEditAction(keyData);
114 AutoClearSelection = true;
115 if (action != null) {
116 motherTextEditorControl.BeginUpdate();
117 try {
118 lock (Document) {
119 // 执行相关的功能操作
120 action.Execute(this);
121 if (SelectionManager.HasSomethingSelected && AutoClearSelection /**//*&& caretchanged*/) {
122 if (Document.TextEditorProperties.DocumentSelectionMode == DocumentSelectionMode.Normal) {
123 SelectionManager.ClearSelection();
124 }
125 }
126 }
127 } finally {
128 motherTextEditorControl.EndUpdate();
129 Caret.UpdateCaretPosition();
130 }
131 return true;
132 }
133 return false;
134}
135
136//输入键的响应(取自TextArea.cs):
137protected override void OnKeyPress(KeyPressEventArgs e)
138{
139 base.OnKeyPress(e);
140 SimulateKeyPress(e.KeyChar);
141 e.Handled = true;
142}
143public void SimulateKeyPress(char ch)
144{
145 if (Document.ReadOnly) {
146 return;
147 }
148
149 if (TextEditorProperties.UseCustomLine == true) {
150 if (SelectionManager.HasSomethingSelected) {
151 if (Document.CustomLineManager.IsReadOnly(SelectionManager.SelectionCollection[0], false))
152 return;
153 } else if (Document.CustomLineManager.IsReadOnly(Caret.Line, false) == true)
154 return;
155 }
156
157 if (ch < ' ') {
158 return;
159 }
160
161 if (!HiddenMouseCursor && TextEditorProperties.HideMouseCursor) {
162 HiddenMouseCursor = true;
163 Cursor.Hide();
164 }
165 CloseToolTip();
166
167 motherTextEditorControl.BeginUpdate();
168 // INSERT char
169 if (!HandleKeyPress(ch)) {
170 switch (Caret.CaretMode) {
171 case CaretMode.InsertMode:
172 InsertChar(ch);
173 break;
174 case CaretMode.OverwriteMode:
175 ReplaceChar(ch);
176 break;
177 default:
178 Debug.Assert(false, "Unknown caret mode " + Caret.CaretMode);
179 break;
180 }
181 }
182
183 int currentLineNr = Caret.Line;
184 int delta = Document.FormattingStrategy.FormatLine(this, currentLineNr, Document.PositionToOffset(Caret.Position), ch);
185
186 motherTextEditorControl.EndUpdate();
187 if (delta != 0) {
188// this.motherTextEditorControl.UpdateLines(currentLineNr, currentLineNr);
189 }
190}
191//输入键通过底层的方法输入字符和更改高亮显示,如下分析(以Insert为例,取自DefaultDocument.cs):
192public void Insert(int offset, string text)
193{
194 if (readOnly) {
195 return;
196 }
197 OnDocumentAboutToBeChanged(new DocumentEventArgs(this, offset, -1, text));
198 DateTime time = DateTime.Now;
199 //增加字符
200 textBufferStrategy.Insert(offset, text);
201
202 time = DateTime.Now;
203 //更新LineSegment, TextWord对象
204 lineTrackingStrategy.Insert(offset, text);
205
206 time = DateTime.Now;
207
208 undoStack.Push(new UndoableInsert(this, offset, text));
209
210 time = DateTime.Now;
211 OnDocumentChanged(new DocumentEventArgs(this, offset, -1, text));
212}
213// 追踪到DefaultLineManager.cs中Insert()方法实际上调用到Replace(offset,length,string.Empty)方法:
214public void Replace(int offset, int length, string text)
215{
216 int lineNumber = GetLineNumberForOffset(offset);
217 int insertLineNumber = lineNumber;
218 if (Remove(lineNumber, offset, length)) {
219 --lineNumber;
220 }
221
222 lineNumber += Insert(insertLineNumber, offset, text);
223
224 int delta = -length;
225 if (text != null) {
226 delta = text.Length + delta;
227 }
228
229 if (delta != 0) {
230 AdaptLineOffsets(lineNumber, delta);
231 }
232 //启用高亮显示分析 ----- 注:此时的 markLines 仅存储变化的相关行,而SetContent时存储的是所有行,因此只是按需分析
233 RunHighlighter();
234}
c, 文件字符的绘制究竟是在何处?
答:在TextView.cs中的public override void Paint(Graphics g, Rectangle rect)函数中,重要函数:PaintLinePart(),辅助绘制函数:DrawDocumentWord(), DrawBracketHighlight(), DrawSpaceMarker(), DrawVerticalRuler() 等

d, 括号匹配在何处被定义和捕捉更新?
答:TextArea.cs中的List bracketshemes = new List();变量存储在查找的匹配项,SearchMatchingBracket()方法中搜索并更新匹配项的显示,相关代码如下:


括号匹配
1//引自TextArea.cs
2List bracketshemes = new List();
3Caret caret;
4public TextArea(TextEditorControl motherTextEditorControl, TextAreaControl motherTextAreaControl)
5{
6 // 省略无关代码
7 bracketshemes.Add(new BracketHighlightingSheme('{', '}'));
8 bracketshemes.Add(new BracketHighlightingSheme('(', ')'));
9 bracketshemes.Add(new BracketHighlightingSheme('[', ']'));
10
11 caret.PositionChanged += new EventHandler(SearchMatchingBracket);
12 // 省略无关代码
13}
14void SearchMatchingBracket(object sender, EventArgs e)
15{
16 if (!TextEditorProperties.ShowMatchingBracket) {
17 textView.Highlight = null;
18 return;
19 }
20 bool changed = false;
21 if (caret.Offset == 0) {
22 if (textView.Highlight != null) {
23 int line = textView.Highlight.OpenBrace.Y;
24 int line2 = textView.Highlight.CloseBrace.Y;
25 textView.Highlight = null;
26 UpdateLine(line);
27 UpdateLine(line2);
28 }
29 return;
30 }
31 foreach (BracketHighlightingSheme bracketsheme in bracketshemes) {
32// if (bracketsheme.IsInside(textareapainter.Document, textareapainter.Document.Caret.Offset)) {
33 Highlight highlight = bracketsheme.GetHighlight(Document, Caret.Offset - 1);
34 if (textView.Highlight != null && textView.Highlight.OpenBrace.Y >=0 && textView.Highlight.OpenBrace.Y < Document.TotalNumberOfLines) {
35 //取消旧匹配项的高亮显示
36 UpdateLine(textView.Highlight.OpenBrace.Y);
37 }
38 if (textView.Highlight != null && textView.Highlight.CloseBrace.Y >=0 && textView.Highlight.CloseBrace.Y < Document.TotalNumberOfLines) {
39 //取消旧匹配项的高亮显示
40 UpdateLine(textView.Highlight.CloseBrace.Y);
41 }
42 textView.Highlight = highlight;
43 if (highlight != null) {
44 changed = true;
45 break;
46 }
47// }
48 }
49 if (changed || textView.Highlight != null) {
50 int line = textView.Highlight.OpenBrace.Y;
51 int line2 = textView.Highlight.CloseBrace.Y;
52 if (!changed) {
53 textView.Highlight = null;
54 }
55 //更新显示新匹配项 OpenBrace
56 UpdateLine(line);
57 //更新显示新匹配项 CloseBrace
58 UpdateLine(line2);
59 }
60}


7、待分析的部分
本篇讨论暂未涉及如下(有价值?)内容的分析:
SyntaxHighlighting实现分析
BookmarkManager, FoldingManager, FormattingManager等
Paint()处理中坐标分析及转换

8、总结
本篇分析对应的《Dissecting a C# Application Inside SharpDevelop》书中的章节:
Chapter 7: Internationalization
Chapter 8: Document Management
Chapter 9: Syntax Highlighting
Chapter 11: Writing the Editor Control

对于该Demo中尚未实现的代码折叠、字符串的查找/替换功能将在下个礼拜读了10,12,13,14章后作进一步分析

SharpDevelop浅析_4_TextEditor_自动完成、代码折叠……

Parser及其应用: Code Completion, Method Insight, Class Scout ...

可见新增功能如下(仅支持.cs文件):
a, 鼠标停留在方法、属性等位置时,会显示出相关的文档描述tooltip
b, 输入时支持自动完成
c, 编辑窗口顶部有类列表和成员(方法、变量等)列表下拉框用以快速浏览、定位
d, 编辑窗口左侧有折叠线用以方法、类等的代码折叠
相应的Demo工程中新增项目如下:
a, SharpEditor: 包含扩展TextEditor的控件, Dom结构, ParserService, 自动完成功能代码等
b, NRefactor : 代码解析功能
c, CSharpBinding: 对应 .cs 文件的具体实现支持

[题外话]:
关于代码解析(Parser)相关的代码,我没看懂,所以在这里只说个大概,更多地谈谈Parser的使用;抛砖引玉,希望有相关经验的网友提供详尽的分析。
前两周工作上的项目实施,每天都搞得比较累,所以这篇文章到现在才写了出来,明天是大年三十了,这个系列的文章也只剩下一篇Windows Form Designer,只能等过了年再放上来喽。
另外,这个系列写完后,暂不打算深究一些没明白的细节,接下来想看下os workflow 或 CommunityServer...



2、Parser实现概述
(1)首先,SharpEditor项目中的Dom下定义了以下重要类:
a, IDecoration及其子类: 代码表现的辅助对象,如IClass, IMethod, IProperty等
b, ResolveResult及其子类: 分析结果对象,如MethodResolveResult, TypeResolveResult等
c, 其它重要类: IExpressionFinder, IParser, IResolve, ICompilationUnit 等

(2)重要服务类:
ParserService: 提供 GetExpressionFinder(), Resolve(), ParseFile()等重要方法,相关重要类: ProjectContentRegistry, DefaultProjectContent, ReflectionProjectContent等
AmbienceService: 提供 IAmbience的实现类用以将分析结果转换为相应的字符描述

(3)Parser分析步骤:
以鼠标悬浮的Tooltip显示为例:DebuggerService根据文件类型返回对应的IExpressionFinder实现类,再根据鼠标位置找到并返回ExpressionResult对象,然后找到适当的IResolver实现类调用Resolve()方法返回结果ResolveResult对象,最后由相应的IAmbience实现类转换成结果字符,并调用e.ShowToolTip(toolTipText);显示。

(4)对于.NET默认类库的分析转换:
默认引进的命名空间的类结构和文档说明一般可以在"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\"目录下找到(如System.dll和System.xml),但是如果每次都要重新分析dll代码结构和xml注释显然是比较花费时间的,于是SharpDevelop采用的方式是将分析过的数据(Dom下的类结构表示数据?,二进制的.dat文件)存储到"C:\Documents and Settings\michael\Local Settings\Temp\SharpDevelop"下,代码结构存储到DomCacheDebug目录下,文档注释存储到DocumentationCacheDebug目录下。
首先在ParserService的CreateDefaultProjectContent()中加载默认命名空间的引用:

CreateDefaultProjectContent()
1static void CreateDefaultProjectContent()
2{
3 LoggingService.Info("Creating default project content");
4 LoggingService.Debug("Stacktrace is:\n" + Environment.StackTrace);
5 defaultProjectContent = new DefaultProjectContent();
6 defaultProjectContent.ReferencedContents.Add(ProjectContentRegistry.Mscorlib);
7 string[] defaultReferences = new string[] {
8 "System",
9 "System.Data",
10 "System.Drawing",
11 "System.Windows.Forms",
12 "System.XML",
13 "Microsoft.VisualBasic",
14 };
15 foreach (string defaultReference in defaultReferences)
16 {
17 //ReferenceProjectItem item = new ReferenceProjectItem(null, defaultReference);
18 IProjectContent pc = ProjectContentRegistry.GetProjectContentForReference(defaultReference);
19 if (pc != null)
20 {
21 defaultProjectContent.ReferencedContents.Add(pc);
22 }
23 }
24}
其中ProjectContentRegistry的GetProjectContentForReference()方法如下:

GetProjectContentForReference()
1public static IProjectContent GetProjectContentForReference(string Include)
2{
3 // 省略部分代码
4 Assembly assembly = GetDefaultAssembly(shortName);
5 ReflectionProjectContent pc;
6 if (assembly != null)
7 {
8 pc = DomPersistence.LoadProjectContentByAssemblyName(assembly.FullName);
9 if (pc == null)
10 {
11 pc = new ReflectionProjectContent(assembly);
12 DomPersistence.SaveProjectContent(pc);
13 }
14 }
15 else
16 {
17 pc = LoadProjectContent(itemFileName, itemInclude);
18 }
19 // 省略部分代码
20 return pc;
21}
22static Assembly GetDefaultAssembly(string shortName)
23{
24 // These assemblies are already loaded by SharpDevelop, so we don't need to load
25 // them in a separate AppDomain.
26 switch (shortName) {
27 case "System": // System != mscorlib !!!
28 return SystemAssembly;
29 case "System.Data":
30 return typeof(System.Data.DataException).Assembly;
31 case "System.Design":
32 return typeof(System.ComponentModel.Design.DesignSurface).Assembly;
33 case "System.DirectoryServices":
34 return typeof(System.DirectoryServices.AuthenticationTypes).Assembly;
35 case "System.Drawing":
36 return typeof(System.Drawing.Color).Assembly;
37 case "System.Web.Services":
38 return typeof(System.Web.Services.WebService).Assembly;
39 case "System.Windows.Forms":
40 return typeof(System.Windows.Forms.Control).Assembly;
41 case "System.Xml":
42 case "System.XML":
43 return typeof(XmlReader).Assembly;
44 case "Microsoft.Build.Engine":
45 return typeof(Microsoft.Build.BuildEngine.BuildSettings).Assembly;
46 case "Microsoft.Build.Framework":
47 return typeof(Microsoft.Build.Framework.LoggerVerbosity).Assembly;
48 default:
49 return null;
50 }
51}
可以看到DomPersistence类的作用即在加载或保存dll的代码结构数据, 如果尚未有分析过的数据,则在ReflectionProjectContnet类的构造函数中加以分析,同时调用XmlDoc类的相关方法加载、保存文档注释数据。

(5)对于文件的转换:
[略]



3、Parser应用: MouseHover Tooltip
注意在SharpEditor项目中有个DebuggerService类,它提供了一个重要方法如下:

BindTextAreaEvent()
1public static void BindTextAreaEvent(TextEditorControl control)
2{
3 TextArea textArea = control.ActiveTextAreaControl.TextArea;
4 //textArea.IconBarMargin.MouseDown += IconBarMouseDown;
5 textArea.ToolTipRequest -= TextArea_ToolTipRequest;
6 textArea.ToolTipRequest += TextArea_ToolTipRequest;
7}
此方法即用以绑定需要鼠标悬浮提示的TextEditor控件,在SharpPad项目的Open菜单类方法中调用此类绑定编辑控件。
注意上面的方法中可以看到ICSharpCode.TextEditor控件是通过其TextArea的ToolTipRequest事件公开给外部,用以分析并由外部提供ToolTip数据,而TextArea_ToolTipRequest(object sender, ToolTipRequestEventArgs e)方法中根据鼠标位置分析出要显示的数据后,最终调用e.ShowToolTip(toolTipText);用以设置提供数据。

TextArea_ToolTipRequest()
1static void TextArea_ToolTipRequest(object sender, ToolTipRequestEventArgs e)
2{
3 try
4 {
5 TextArea textArea = (TextArea)sender;
6 if (e.ToolTipShown) return;
7 if (oldToolTipControl != null && !oldToolTipControl.AllowClose) return;
8 if (!CodeCompletionOptions.TooltipsEnabled) return;
9
10 if (CodeCompletionOptions.TooltipsOnlyWhenDebugging)
11 {
12 if (currentDebugger == null) return;
13 if (!currentDebugger.IsDebugging) return;
14 }
15
16 if (e.InDocument)
17 {
18 Point logicPos = e.LogicalPosition;
19 IDocument doc = textArea.Document;
20 IExpressionFinder expressionFinder = ParserService.GetExpressionFinder(textArea.MotherTextEditorControl.FileName);
21 if (expressionFinder == null)
22 return;
23 LineSegment seg = doc.GetLineSegment(logicPos.Y);
24 if (logicPos.X > seg.Length - 1)
25 return;
26 string textContent = doc.TextContent;
27 ExpressionResult expressionResult = expressionFinder.FindFullExpression(textContent, seg.Offset + logicPos.X);
28 string expression = expressionResult.Expression;
29 if (expression != null && expression.Length > 0)
30 {
31 // Look if it is variable
32 ResolveResult result = ParserService.Resolve(expressionResult, logicPos.Y + 1, logicPos.X + 1, textArea.MotherTextEditorControl.FileName, textContent);
33 bool debuggerCanShowValue;
34 string toolTipText = GetText(result, expression, out debuggerCanShowValue);
35 DebuggerGridControl toolTipControl = null;
36 if (toolTipText != null)
37 {
38 if (Control.ModifierKeys == Keys.Control)
39 {
40 toolTipText = "expr: " + expressionResult.ToString() + "\n" + toolTipText;
41 }
42 else if (debuggerCanShowValue && currentDebugger != null)
43 {
44 toolTipControl = currentDebugger.GetTooltipControl(expressionResult.Expression);
45 toolTipText = null;
46 }
47 }
48 if (toolTipText != null)
49 {
50 e.ShowToolTip(toolTipText);
51 }
52 if (oldToolTipControl != null)
53 {
54 Form frm = oldToolTipControl.FindForm();
55 if (frm != null) frm.Close();
56 }
57 if (toolTipControl != null)
58 {
59 toolTipControl.ShowForm(textArea, logicPos);
60 }
61 oldToolTipControl = toolTipControl;
62 }
63 }
64 }
65 catch (Exception ex)
66 {
67 MessageBox.Show(ex.ToString());
68 }
69}

4、Parser应用: CodeCompletion & MethodInsight
ICSharpCode.TextEditor控件使用ICompletionData类以及ICompletionDataProvider接口(GenerateCompletionData()方法)定义数据自动完成列表的数据及其提供者: SharpEditor项目中的AbstractCodeCompletionDataProvider类中由GetExpression()及AddResolveResults()两个辅助方法生成数据,代码如下:

AbstractCodeCompletionDataProvider 类
1protected ArrayList completionData = null;
2//
3protected ExpressionResult GetExpression(TextArea textArea)
4{
5 IDocument document = textArea.Document;
6 IExpressionFinder expressionFinder = ParserService.GetExpressionFinder(fileName);
7 if (expressionFinder == null) {
8 return new ExpressionResult(TextUtilities.GetExpressionBeforeOffset(textArea, textArea.Caret.Offset));
9 } else {
10 ExpressionResult res = expressionFinder.FindExpression(document.GetText(0, textArea.Caret.Offset), textArea.Caret.Offset - 1);
11 if (overrideContext != null)
12 res.Context = overrideContext;
13 return res;
14 }
15}
16protected void AddResolveResults(ResolveResult results, ExpressionContext context)
17{
18 insertedElements.Clear();
19 insertedPropertiesElements.Clear();
20 insertedEventElements.Clear();
21
22 if (results != null) {
23 AddResolveResults(results.GetCompletionData(ParserService.CurrentProjectContent), context);
24 }
25}
26protected void AddResolveResults(ICollection list, ExpressionContext context)
27{
28 if (list == null) {
29 return;
30 }
31 completionData.Capacity += list.Count;
32 CodeCompletionData suggestedData = null;
33 foreach (object o in list) {
34 if (context != null && !context.ShowEntry(o))
35 continue;
36 CodeCompletionData ccd = CreateItem(o, context);
37 if (object.Equals(o, context.SuggestedItem))
38 suggestedData = ccd;
39 if (ccd != null && !ccd.Text.StartsWith("___"))
40 completionData.Add(ccd);
41 }
42 if (context.SuggestedItem != null) {
43 if (suggestedData == null) {
44 suggestedData = CreateItem(context.SuggestedItem, context);
45 if (suggestedData != null) {
46 completionData.Add(suggestedData);
47 }
48 }
49 if (suggestedData != null) {
50 completionData.Sort();
51 this.DefaultIndex = completionData.IndexOf(suggestedData);
52 }
53 }
54}
CommentCompletionDataProvider类提供注释的自动完成,相关代码如下:

CommentCompletionDataProvider 类
1string[][] commentTags = new string[][] {
2 new string[] {"c", "marks text as code"},
3 new string[] {"code", "marks text as code"},
4 new string[] {"example", "A description of the code example\n(must have a tag inside)"},
5 new string[] {"exception cref=\"\"", "description to an exception thrown"},
6 new string[] {"list type=\"\"", "A list"},
7 new string[] {"listheader", "The header from the list"},
8 new string[] {"item", "A list item"},
9 new string[] {"term", "A term in a list"},
10 new string[] {"description", "A description to a term in a list"},
11 new string[] {"para", "A text paragraph"},
12 new string[] {"param name=\"\"", "A description for a parameter"},
13 new string[] {"paramref name=\"\"", "A reference to a parameter"},
14 new string[] {"permission cref=\"\"", ""},
15 new string[] {"remarks", "Gives description for a member"},
16 new string[] {"include file=\"\" path=\"\"", "Includes comments from other files"},
17 new string[] {"returns", "Gives description for a return value"},
18 new string[] {"see cref=\"\"", "A reference to a member"},
19 new string[] {"seealso cref=\"\"", "A reference to a member in the seealso section"},
20 new string[] {"summary", "A summary of the object"},
21 new string[] {"value", "A description of a property"}
22};
23public override ICompletionData[] GenerateCompletionData(string fileName, TextArea textArea, char charTyped)
24{
25 caretLineNumber = textArea.Caret.Line;
26 caretColumn = textArea.Caret.Column;
27 LineSegment caretLine = textArea.Document.GetLineSegment(caretLineNumber);
28 string lineText = textArea.Document.GetText(caretLine.Offset, caretLine.Length);
29 if (!lineText.Trim().StartsWith("///") && !lineText.Trim().StartsWith("'''")) {
30 return null;
31 }
32
33 ArrayList completionData = new ArrayList();
34 foreach (string[] tag in commentTags) {
35 completionData.Add(new CommentCompletionData(tag[0], tag[1]));
36 }
37 return (ICompletionData[])completionData.ToArray(typeof(ICompletionData));
38}
类似地,ICSharpCode.TextEditor控件提供了IInsightDataProvider接口(GetInsightData()方法),SharpEditor项目中实现类MethodInsightDataProvider和IndexerInsightDataProvider提供相关数据。

除了上面需要提供数据的类外,还要绑定TextEditor的TextArea的KeyEventHandler事件(详见SharpEditor项目的SharpDevelopTextAreaControl.cs),其中辅助类的处理代码如下:

KeyEventHandler 事件响应
1// 默认处理 DefaultCodeCompletionBinding(详见SharpEditor项目的CodeCompletionBinding.cs):
2public virtual bool HandleKeyPress(SharpDevelopTextAreaControl editor, char ch)
3{
4 switch (ch)
5 {
6 case '(':
7 if (enableMethodInsight && CodeCompletionOptions.InsightEnabled)
8 {
9 editor.ShowInsightWindow(new MethodInsightDataProvider());
10 return true;
11 }
12 else
13 {
14 return false;
15 }
16 case '[':
17 if (enableIndexerInsight && CodeCompletionOptions.InsightEnabled)
18 {
19 editor.ShowInsightWindow(new IndexerInsightDataProvider());
20 return true;
21 }
22 else
23 {
24 return false;
25 }
26 case '<':
27 if (enableXmlCommentCompletion)
28 {
29 editor.ShowCompletionWindow(new CommentCompletionDataProvider(), ch);
30 return true;
31 }
32 else
33 {
34 return false;
35 }
36 case '.':
37 if (enableDotCompletion)
38 {
39 editor.ShowCompletionWindow(new CodeCompletionDataProvider(), ch);
40 return true;
41 }
42 else
43 {
44 return false;
45 }
46 case ' ':
47 if (!CodeCompletionOptions.KeywordCompletionEnabled)
48 return false;
49 string word = editor.GetWordBeforeCaret();
50 if (word != null)
51 return HandleKeyword(editor, word);
52 else
53 return false;
54 default:
55 return false;
56 }
57}
58// .cs 文件的处理(详见CSharpBinding项目的CSharpCompletionBinding.cs):
59public override bool HandleKeyPress(SharpDevelopTextAreaControl editor, char ch)
60{
61 Parser.ExpressionFinder ef = new Parser.ExpressionFinder(editor.FileName);
62 int cursor = editor.ActiveTextAreaControl.Caret.Offset;
63 ExpressionContext context = null;
64 if (ch == '(')
65 {
66 if (CodeCompletionOptions.KeywordCompletionEnabled)
67 {
68 switch (editor.GetWordBeforeCaret().Trim())
69 {
70 case "for":
71 case "lock":
72 context = ExpressionContext.Default;
73 break;
74 case "using":
75 context = ExpressionContext.TypeDerivingFrom(ReflectionReturnType.Disposable.GetUnderlyingClass(), false);
76 break;
77 case "catch":
78 context = ExpressionContext.TypeDerivingFrom(ReflectionReturnType.Exception.GetUnderlyingClass(), false);
79 break;
80 case "foreach":
81 case "typeof":
82 case "sizeof":
83 case "default":
84 context = ExpressionContext.Type;
85 break;
86 }
87 }
88 if (context != null)
89 {
90 if (IsInComment(editor)) return false;
91 editor.ShowCompletionWindow(new CtrlSpaceCompletionDataProvider(context), ch);
92 return true;
93 }
94 else if (EnableMethodInsight && CodeCompletionOptions.InsightEnabled)
95 {
96 editor.ShowInsightWindow(new MethodInsightDataProvider());
97 return true;
98 }
99 return false;
100 }
101 else if (ch == '[')
102 {
103 LineSegment line = editor.Document.GetLineSegmentForOffset(cursor);
104 if (TextUtilities.FindPrevWordStart(editor.Document, cursor) <= line.Offset)
105 {
106 // [ is first character on the line
107 // -> Attribute completion
108 editor.ShowCompletionWindow(new AttributesDataProvider(), ch);
109 return true;
110 }
111 }
112 else if (ch == ',' && CodeCompletionOptions.InsightRefreshOnComma && CodeCompletionOptions.InsightEnabled)
113 {
114 // Show MethodInsightWindow or IndexerInsightWindow
115 string documentText = editor.Text;
116 int oldCursor = cursor;
117 string textWithoutComments = ef.FilterComments(documentText, ref cursor);
118 int commentLength = oldCursor - cursor;
119 if (textWithoutComments != null)
120 {
121 Stack parameters = new Stack();
122 char c = '\0';
123 while (cursor > 0)
124 {
125 while (--cursor > 0 &&
126 ((c = textWithoutComments[cursor]) == ',' ||
127 char.IsWhiteSpace(c))) ;
128 if (c == '(')
129 {
130 ShowInsight(editor, new MethodInsightDataProvider(cursor + commentLength, true), parameters, ch);
131 return true;
132 }
133 else if (c == '[')
134 {
135 ShowInsight(editor, new IndexerInsightDataProvider(cursor + commentLength, true), parameters, ch);
136 return true;
137 }
138 string expr = ef.FindExpressionInternal(textWithoutComments, cursor);
139 if (expr == null || expr.Length == 0)
140 break;
141 parameters.Push(ParserService.Resolve(new ExpressionResult(expr),
142 editor.ActiveTextAreaControl.Caret.Line,
143 editor.ActiveTextAreaControl.Caret.Column,
144 editor.FileName,
145 documentText));
146 cursor = ef.LastExpressionStartPosition;
147 }
148 }
149 }
150 else if (ch == '=')
151 {
152 LineSegment curLine = editor.Document.GetLineSegmentForOffset(cursor);
153 string documentText = editor.Text;
154 int position = editor.ActiveTextAreaControl.Caret.Offset - 2;
155
156 if (position > 0 && (documentText[position + 1] == '+'))
157 {
158 ExpressionResult result = ef.FindFullExpression(documentText, position);
159
160 if (result.Expression != null)
161 {
162 ResolveResult resolveResult = ParserService.Resolve(result, editor.ActiveTextAreaControl.Caret.Line, editor.ActiveTextAreaControl.Caret.Column, editor.FileName, documentText);
163 if (resolveResult != null && resolveResult.ResolvedType != null)
164 {
165 IClass underlyingClass = resolveResult.ResolvedType.GetUnderlyingClass();
166 if (underlyingClass != null && underlyingClass.IsTypeInInheritanceTree(ProjectContentRegistry.Mscorlib.GetClass("System.MulticastDelegate")))
167 {
168 EventHandlerCompletitionDataProvider eventHandlerProvider = new EventHandlerCompletitionDataProvider(result.Expression, resolveResult);
169 eventHandlerProvider.InsertSpace = true;
170 editor.ShowCompletionWindow(eventHandlerProvider, ch);
171 }
172 }
173 }
174 }
175 }
176 else if (ch == ';')
177 {
178 LineSegment curLine = editor.Document.GetLineSegmentForOffset(cursor);
179 // don't return true when inference succeeds, otherwise the ';' won't be added to the document.
180 TryDeclarationTypeInference(editor, curLine);
181 }
182
183 return base.HandleKeyPress(editor, ch);
184}

5、Parser应用: QuickClassBrowserPanel
QuickClassBrowserPanel即编辑窗口顶部的类及成员快速浏览窗口,其实现如下:
首先在SharpDevelopTextAreaControl类中重写override void OnFileNameChanged(EventArgs e)方法,即打开新文件时,调用ActivateQuickClassBrowserOnDemand()来根据是否有能够分析此文件的Parser来决定是否显示控件,如果可以显示,则实例化出一个新对象,并添加进来。实例化QuickClassBrowserPanel对象的相关代码如下:

QuickClassBrowserPanel
1private System.Windows.Forms.ComboBox classComboBox;
2private System.Windows.Forms.ComboBox membersComboBox;
3ICompilationUnit currentCompilationUnit;
4//
5public QuickClassBrowserPanel(SharpDevelopTextAreaControl textAreaControl)
6{
7 InitializeComponent();
8 this.membersComboBox.MaxDropDownItems = 20;
9
10 base.Dock = DockStyle.Top;
11 this.textAreaControl = textAreaControl;
12 this.textAreaControl.ActiveTextAreaControl.Caret.PositionChanged += new EventHandler(CaretPositionChanged);
13}
14void CaretPositionChanged(object sender, EventArgs e)
15{
16 // ignore simple movements
17 if (e != EventArgs.Empty) {
18 return;
19 }
20 try {
21
22 ParseInformation parseInfo = ParserService.GetParseInformation(textAreaControl.FileName);
23 if (parseInfo != null) {
24 if (currentCompilationUnit != (ICompilationUnit)parseInfo.MostRecentCompilationUnit) {
25 currentCompilationUnit = (ICompilationUnit)parseInfo.MostRecentCompilationUnit;
26 if (currentCompilationUnit != null) {
27
28 FillClassComboBox(true);
29 FillMembersComboBox();
30 }
31 }
32 UpdateClassComboBox();
33 UpdateMembersComboBox();
34 }
35 } catch (Exception ex) {
36 MessageService.ShowError(ex);
37 }
38}
可以看到类、成员列表的ComboBox数据的填充是在textAreaControl.ActiveTextAreaControl.Caret.PositionChanged事件后执行,因为光标位置的改变可能需要同步更新顶部列表框的显示;注意两个列表的列表项使用自定义类ComboBoxItem, 包括成员的Line, Column等信息;该类中绑定两个列表的SelectedIndexChange事件用以在编辑器中定位相应的类或成员位置:

ComboBoxSelectedIndexChanged()
1void ComboBoxSelectedIndexChanged(object sender, System.EventArgs e)
2{
3 ComboBox comboBox = (ComboBox)sender;
4 if (autoselect) {
5 ComboBoxItem item = (ComboBoxItem)comboBox.Items[comboBox.SelectedIndex];
6 if (item.IsInCurrentPart)
7 {
8 textAreaControl.ActiveTextAreaControl.Caret.Position = new Point(item.Column, item.Line);
9 textAreaControl.ActiveTextAreaControl.TextArea.Focus();
10 }
11 }
12}

6、Parser应用: Folding
ICSharpCode.TextEditor控件提供了IFoldingStrategy接口用以定义代码折叠需实现的方法:

IFoldingStrategy 接口
1/**////
2/// This interface is used for the folding capabilities
3/// of the textarea.
4///

5public interface IFoldingStrategy
6{
7 /**////
8 /// Calculates the fold level of a specific line.
9 ///

10 List GenerateFoldMarkers(IDocument document, string fileName, object parseInformation);
11}
SharpEditor项目中,SharpDevelopTextAreaControl在构造函数中指明了FoldingStrategy:

SharpDevelopTextAreaControl 构造函数
1public SharpDevelopTextAreaControl()
2{
3 Document.FoldingManager.FoldingStrategy = new ParserFoldingStrategy();
4
5 // 由于UpdateClassMemberBookmarks()方法中以Bookmark形式显示方法、属性、变量等
6 // 故此处需要定义Bookmark Factory 以使上面的Bookmark 与用户加入的书签区别开
7 Document.BookmarkManager.Factory = new Bookmarks.SDBookmarkFactory(Document.BookmarkManager);
8}
9protected override void OnFileNameChanged(EventArgs e)
10{
11 base.OnFileNameChanged(e);
12 ActivateQuickClassBrowserOnDemand();
13 ForceFoldingUpdate();
14}
15void ForceFoldingUpdate()
16{
17 ParseInformation parseInfo = ParserService.GetParseInformation(FileName);
18 if (parseInfo == null)
19 {
20 parseInfo = ParserService.ParseFile(FileName, Document.TextContent, false, false);
21 }
22 Document.FoldingManager.UpdateFoldings(FileName, parseInfo);
23
24 // 在编辑器的类、属性、变量等行左侧显示相应图标
25 UpdateClassMemberBookmarks(parseInfo);
26}
同样需要在打开新文件时调用更新代码折叠标签,代码折叠FoldMarker生成的具体实现详见SharpEditor项目的ParserFoldingStrategy.cs


7、总结
上面简要介绍了Parser的部分实现, 期待有相关经验的网友指定我的理解错误或给予补充解释。
以及Parser的部分应用:MouseHover Tooltip, CodeCompletion & MethodInsight, QuickClassBrowserPanel, Folding;SharpDevelop中的其它应用如类视图ClassBrowser, 对象浏览AssemblyPad(SharpDevelop2.0源码中未见到?).

SharpDevelop浅析_5_Windows Forms Designer
自己动手创建应用程序界面设计器

功能概述:
a, 窗体左侧为工具栏,可以单击、双击、拖曳的方式来添加控件
b, 窗体右侧为属性(事件)窗口及控件(浏览)选择Combo
c, 窗体中部包括设计器及代码查看Tab页
d, 窗体顶部实现了编辑、对齐、运行等命令项

2、Demo实现简述
a, 设计器的核心是.NET框架提供的DesignSurface类、ServiceContainer类
b, 设计器的扩展点(自定义部分)是通过向ServiceContainer添加自动定义服务类(IOC模式?)或订阅服务类的事件……
c, Demo中的使用或创建的服务类包含了ISelectionService, IComponentChangeService MenuCommandService, CustomToolboxService, NameCreationService, DesignerEventService, EventBindingService.
d, 补充说明: Demo左侧工具栏项目是通过\data\SharpDevelopControlLibrary.xml配置文件定义;窗口中部的代码查看页中使用了SharpDevelop的TextEditorControl.

3、参考资料
关于自定义窗体设计器的具体设计过程请参照如下资源:

Create And Host Custom Designers With The .NET Framework 2.0
http://msdn.microsoft.com/msdnmag/issues/06/03/DesignerHosting/default.aspx

利用 .NET Framework 2.0 创建并宿主自定义的设计
http://www.microsoft.com/china/MSDN/library/netFramework/netframework/ DesignerHosting.mspx?mfr=true

Hosting Windows Forms Designers
http://www.divil.co.uk/net/articles/designers/hosting.asp

沧海月明 "写Form设计器尝试"系列
http://www.cnblogs.com/panjiwen/category/36995.html

Demo中左侧工具栏的控件使用了 纶巾客 写的一个控件:制作VS风格的Toolbox控件
http://www.cnblogs.com/guanjinke/archive/2007/01/10/617092.html

对该控件的主要修改如下:
ToolBoxItem类添加了Tag及Image属性以存储Tag数据及显示图片
ToolBox增加了SelectedItemChanged, ItemDoubleClicked, ItemDragStart事件

Wednesday, June 14, 2006

 

Some notes on SharpDevelop - 1

1.
From http://passos.cnblogs.com/archive/2004/10/04/48950.html
http://passos.cnblogs.com/archive/2004/10/07/49654.html
http://passos.cnblogs.com/archive/2004/10/10/50652.html
http://passos.cnblogs.com/archive/2004/10/15/52513.html

SharpDevelop代码分析 (一、序+基本概念)


最近开始学习.Net,遇到了一个比较不错的开源的IDE SharpDevelop。这个开发工具是使用C#开发的,比较吸引我的一点就是它是采用了和Eclipse类似的插件技术来实现整个系统的。而这个插件系统是我最感兴趣的地方,因此开始了一段代码的研究。在本篇之后,我会陆续把我研究的心得写下来。由于是在网吧上网,有诸多不便,因此可能会拖比较长的时间。



一、基本概念

首先,我们先来对 SharpDevelop 有一个比较感性的认识。你可以从这里下载到它的可执行程序和代码包 http://www.icsharpcode.com/ ,安装的废话就不说了,先运行一下看看。感觉跟VS很像吧?不过目前的版本是1.0.0.1550,还有很多地方需要完善。关于代码和系统结构,SharpDevelop的三个作者写了一本书,各位看官可以参考一下,不过我看过之后还是有很多地方不太理解。

然后,让我来解释一下什么叫插件以及为什么要使用插件系统。我们以往的系统,开发人员编译发布之后,系统就不允许进行更改和扩充了,如果要进行某个功能的扩充,则必须要修改代码重新编译发布。这就给我们带来了比较大的不方便。解决的方法有很多,例如提供配置等等方法。在解决方案之中,插件是一个比较好的解决方法。大家一定知道PhotoShop、WinAmp吧,他们都有“插件”的概念,允许其他开发人员根据系统预定的接口编写扩展功能(例如PhotoShop中各种各样的滤镜)。所谓的插件就是系统的扩展功能模块,这个模块是以一个独立文件的形式出现的,与系统是相对独立。在系统设计期间并不知道插件的具体功能,仅仅是在系统中为插件留下预定的接口,系统启动的时候根据插件的配置寻找插件,根据预定的接口把插件挂接到系统中。

这样的方式带来什么样的优点呢?首先是系统的扩展性大大的增强了,如果我们在系统发布后需要对系统进行扩充,不必重新编译,只需要修改插件就可以了。其次有利与团队开发,各个功能模块由于是以插件的形式表现在系统中,系统的每日构造就很简单了,不会因为某个模块的错误而导致整个系统的BUILD失败。失败的仅仅是一个插件而已。

PhotoShop和Winamp的插件系统是比较简单的,他们首先实现了一个基本的系统,然后在这个系统的基础上挂接其他扩展的功能插件。而SharpDevelop的插件系统更加强大,它的整个系统的基础就仅仅是一个插件管理系统,而你看到的所有的界面、功能统统都是以插件的形式挂入的。在这样的一个插件系统下,我们可以不修改基本系统,仅仅使用插件就构造出各种各样不同的系统。

现在让我们来看看它的插件系统。进入到SharpDevelop的安装目录中,在Bin目录下的SharpDevelop.exe 和 SharpDevelop.Core.dll是这个系统的基本的插件系统。在Addins目录下有两个后缀是addin的文件,其中一个 SharpDevelopCore.addin 就是它的核心插件的定义(配置)文件,里面定义的各个功能模块存在于Bin\Sharpdevelop.Base.dll 文件中,另外还有很多其他的插件定义在Addins目录下的addin文件中。

分析SharpDevelop的代码,首先要弄清楚几个基本的概念,这些概念和我以前的预想有一些区别,我深入了代码之后才发现我的困惑所在。

1、AddInTree 插件树
SharpDevelop 中的插件被组织成一棵插件树结构,树的结构是通过 Extension(扩展点)中定义的Path(路径)来定义的,类似一个文件系统的目录结构。系统中的每一个插件都在配置文件中指定了 Extension,通过Extension中指定的 Path 挂到这棵插件树上。在系统中可以通过 AddTreeSingleton对象来访问各个插件,以实现插件之间的互动。

2、 AddIn 插件
在 SharpDevelop 的概念中,插件是包含多个功能模块的集合(而不是我过去认为的一个功能模块)。在文件的表现形式上是一个addin配置文件,在系统中对应 AddIn 类。

3、Extension 扩展点
SharpDevelop中的每一个插件都会被挂到 AddInTree(插件树) 中,而具体挂接到这个插件树的哪个位置,则是由插件的 Extension 对象中的 Path 指定的。在addin 配置文件中,对应于 Extension 。例如下面这个功能模块的配置

Extension path = "/SharpDevelop/Workbench/Ambiences"
Class id = ".NET" class = "ICSharpCode.SharpDevelop.Services.NetAmbience"/
/Extension
指定了扩展点路径为 /SharpDevelop/Workbench/Ambiences ,也就是在插件树中的位置。

4、Codon
这个是一个比较不好理解的东西,在 SharpDevelop 的三个作者写的书的中译版中被翻译为密码子,真是个糟糕的翻译,可以跟Handle(句柄)有一拼了。词典中还有一个翻译叫“基码”,我觉得这个也不算好,不过还稍微有那么一点意思。(这里我原来误写为“代码子”,在评论中有位仁兄说这个翻译不错,现在我觉得也好像确实不错 ^o^)
根据我对代码的理解,Codon 的功能是描述(包装)一个功能模块(一个功能模块对应一个实现了具体功能的 Command 类)。为了方便访问各个插件中的功能模块, Codon 给各种功能定义了基本的属性,分别是 ID (功能模块的标识),Name (功能模块的类型。别误会,这个Name 是addin文件定义中Codon的XML结点的名称,ID才是真正的名称),其中Name可能是Class(类)、MenuItem(菜单项)、Pad(面板)等等。根据具体的功能模块,可以继承Codon定义其他的一些属性,SharpDevelop中就定义了 ClassCodon、MenuItemCodon、PadCodon等等,你可以根据需要自己定义其他类型的Codon。在addin定义文件中,Codon对应于 Extension 标签下的内容。例如下面这个定义

Extension path = "/SharpDevelop/Workbench/Ambiences"
Class id = ".NET" class = "ICSharpCode.SharpDevelop.Services.NetAmbience"/
/Extension

Extension ... 内部定义了一个Codon,Class ... 表示该Codon是一个 Class(类),接着定义了该Codon的 ID和具体实现该Codon的类名ICSharpCode.SharpDevelop.Services.NetAmbience。运行期间将通过反射来找到对应的类并创建出来,这一点也是我们无法在以前的语言中实现的。

再例如这一个定义

Extension path = "/SharpDevelop/Views/ProjectBrowser/ContextMenu/CombineBrowserNode"
MenuItem id = "Compile"
label = "${res:XML.MainMenu.RunMenu.Compile}"
class = "ICSharpCode.SharpDevelop.Commands.Compile"/
MenuItem id = "CompileAll"
label = "${res:XML.MainMenu.RunMenu.CompileAll}"
class = "ICSharpCode.SharpDevelop.Commands.CompileAll"/
MenuItem id = "CombineBuildGroupSeparator" label = "-" /
.
/Extension

这个扩展点中定义了三个菜单项,以及各个菜单项的名字、标签和实现的类名。这里的Codon就对应于系统中的MenuCodon对象。

5、Command 命令
正如前文所述,Codon描述了一个功能模块,而每个功能模块都是一个 ICommand 的实现。最基本的 Command 是 AbstractCommand,根据Codon的不同对应了不同的 Command。例如 MenuItemCodon 对应 MenuItemCommand 等等。

6、Service 服务
插件系统中,有一些功能是整个系统都要使用的,例如文件访问、资源、消息等等。这些功能都作为插件系统的一个基本功能为整个系统提供服务,我们就叫“服务”好了。为了便于访问,这些服务都统一通过 ServiceManager 来管理。其实服务也是一种类型的插件,它们的扩展点路径在目录树中的 /Workspace/Services 中。

理解了这几个基本的概念之后,就可以看看 SharpDevelop 的代码了。从 src\main\startup.cs 看起吧,之后是addin.cs、addinTree.cs 等等。

写了两个小时了,休息一下。且听下回分解吧。

SharpDevelop源码分析 (二、主程序+隐藏的初始化)
在大学课程里面,我对于模拟电路总是搞不清楚,直到现在也是这样。我总觉得电路图很奇怪,总会问“这部分电路是做什么用的”、“为什么会有这样的效果”。在我的脑海里面,每部分的电路都应该有一定的用处,可是我总是看不明白。我妈妈说,我的思路被软件所固化的太久了,看电路图不应该总是一个个模块的看,正确的方法应该是从电源的一极顺着电路看,一直看到电源的另一极。我现在仍然不懂看电路图,可是以我看代码的经验来说,我觉得分析源代码按照这样的思路来看会比较容易把脉络理清楚。
在SharpDevelop的代码中,由于很多的接口和插件的原因,很多代码在看到某个地方会突然失去函数/方法调用的线索。例如看某个函数的实现的时候会跳到一个接口里面去,那是因为这部分功能在运行期才会给一个实现了这个接口的对象来进行具体的执行。从这个角度来说,设计模式也给我们研究代码稍微带来了一点小小的难度。在看Linux下源代码的时候也经常遇到这种问题,在这个时候寻找代码线索比较好的方法是用一个文本搜索工具来搜索相关的关键字。在Linux下我经常会用grep,Windows下面类似UltraEdit的“批量文件查找”功能会很好用(或者“Search And Replace”之类的工具)。这个是我读代码的一点小小的经验,如果你知道有更好的方法,请告诉我让我也学习一下 ? 。
我不想大段大段的贴代码出来占地方(空间、带宽,还有各位看官的注意力),在需要的地方我会贴上主要的代码,因此最好能够找代码来对应着看。把代码包解压缩,我把它解到了“F:\SharpDevelop”(如果没有说明,下文都是以此为代码的根目录了)。由于SharpDevelop本身对于察看代码不是很方便,没有“转到定义”之类的功能,因此我建议你把它的代码转成VS的工程来看。不过很可惜,SharpDevelop的工程导出功能现在有问题,如果导出\src\SharpDevelop.cmbx 这个总的复合工程的话会失败(我记得RC1版本是可以成功的,不知道为什么后来的版本反而会出问题),所以只能一个一个工程的导出。
好了,让我们来看SharpDevelop的代码吧。
1、起点
在主程序的起点在\src\Main\StartUp\SharpDevelopMain.cs,找到Main函数这就是整个程序的起点了。开始的部分是显示封面窗体并加上命令行控制,其中SplashScreenForm 定义在\src\Main\Base\Gui\Dialogs\SplashScreen.cs文件中,这部分我就不多说了。之后是

Application.ThreadException += new ThreadExceptionEventHandler(ShowErrorBox);

SharpDevelop为了有效的进行错误报告,因此自己进行了异常的控制。系统出现异常的时候,SharpDevelop会拦截下来弹出它自己的异常提示报告对话框。这个代码就是在这一行实现的。其中 ShowErrorBox 这个方法就在类SharpDevelopMain中,ExceptionBox 定义在\src\Main\StartUp\Dialogs\ExceptionBox.cs中。如果需要进行自己的异常控制,可以学习一下这里的技巧。

2、充满玄机的初始化

string [] addInDirs = ICSharpCode.SharpDevelop.AddInSettingsHandler.GetAddInDirectories( out ignoreDefaultPath );
AddInTreeSingleton.SetAddInDirectories(addInDirs, ignoreDefaultPath);
通过AddInSettingsHandler取得插件的目录,并告知AddInTreeSingleton。AddInSettingsHandler定义在\src\Main\StartUp\Dialogs\AddInTreeSettingsHandler.cs中,它通过读取系统配置(App.config)文件中的AddInDirectory节点的Path属性来确定插件的目录位置,或者你也可以通过自己定义的AddInDirectories节来指定插件目录。如果你没有做这些配置,默认的目录在SharpDevelop运行目录的..\Addins目录下。

ServiceManager.Services.AddService(new MessageService());
ServiceManager.Services.AddService(new ResourceService());
ServiceManager.Services.AddService(new IconService());
通过ServiceManager(服务管理器)加入三个系统默认的服务,消息服务、资源服务、图标服务。这三个服务中,消息服务是显示各种信息提示,另外两个是属于系统的资源,SharpDevelop通过服务来进行统一调用和管理。
ServiceManager.Services.InitializeServicesSubsystem("/Workspace/Services");

初始化其他的服务。SharpDevelop把服务定义在插件树的/Workspace/Services这个路径中,凡是在这个路径下的插件都被认为是服务,因此如果你自己定义了一个服务的话,也需要挂到这个路径下(这里就是系统服务的扩展点了)。

注意!这一步中,在我们的眼皮子底下悄悄的进行了一个重要的初始化工作。各位看官请看,ServiceManager 定义在\src\Main\Core\Services\ ServiceManager.cs文件中,察看它的InitializeServicesSubsystem方法,我们发现这样一行

AddServices((IService[])AddInTreeSingleton.AddInTree.GetTreeNode(servicesPath).BuildChildItems(this).ToArray(typeof(IService)));
在这里,AddInTreeSingleton首次调用了AddInTree(插件树)的实例。按照Singleton模式,只有在首次调用的时候才会初始化实例,这里也是同样如此。整个系统的AddInTree是在这一步中进行了初始化工作,稍候我们将详细介绍AddInTree如何进行初始化工作,先顺便看看服务的初始化。在ServiceManager的InitializeServicesSubsystem方法中,通过AddInTree检索服务插件路径下的所有配置,并通过它来读取、建立具体的对象,然后加入到服务列表中。之后通过一个循环,逐个的调用各个服务的InitializeService方法初始化服务。

AddInTree的初始化工作容我们稍候再看,先把主体的代码看完。


commands = AddInTreeSingleton.AddInTree.GetTreeNode("/Workspace/Autostart").BuildChildItems(null);
for (int i = 0; i commands.Count - 1; ++i)
{
((ICommand)commands[i]).Run();
}
/Workspace/Autostart是系统自动运行命令的扩展点路径,定义在这个路径下的插件会在系统启动的时候自动运行。在这里,通过插件树初始化建立处于这个路径下的Command(命令),并逐一执行。BuildChildItems方法的功能是建立这个扩展点下的Command列表,我会在介绍AddTree的时候具体说明它的实现。

主程序代码的最后,初始化完毕、关闭封面窗体,然后执行命令列表中最后一个命令(也就是系统的主界面)。在主界面退出的时候,系统卸载所有的服务。

在这部分代码中,我们知道了两个系统指定的扩展点路径 /Workspace/Services 和 /Workspace/Autostart ,我们实现服务和指定系统自动运行命令的时候就可以挂到这两个扩展点路径下了。
托反射的福,ServiceManager.Services可以通过类型(接口)来查找具体的实例,也就是GetServices方法。但是ServiceManager的具体实现我们可以容后再看,这里已经不是最紧要的部分了。
接下来,我们来看看整个插件系统的核心-AddinTree的代码,看看它是如何通过插件配置进行初始化并建立起整个系统的插件树骨干。

SharpDevelop源码分析 (三、插件系统)

三、插件系统

上回书说到SharpDevelop入口Main函数的结构,ServiceManager.Service在InitializeServicesSubsystem方法中首次调用了AddInTreeSingleton的AddInTree实例,AddInTree在这里进行了初始化。本回进入AddInTree着重讲述SharpDevelop的插件系统。在叙述的时候为了方便起见,对于“插件”和插件具体的“功能模块”这两个词不会特别的区分,各位看官可以从上下文分辨具体的含义(而事实上,SharpDevelop中的“插件”是指.addin配置文件,每一个“插件”都可能会包含多个“功能模块”)。

1、插件的配置
既然说到插件系统,那么我们先来看一看SharpDevelop插件系统的组织形式。
很多时候,同一个事物从不同的角度来看会得出不一样的结论,SharpDevelop的插件系统也是如此。在看SharpDevelop的代码以前,按照我对插件的理解,我认为所谓的“插件”就是代表一个功能模块,插件的配置就是描述该插件并指定如何把这个插件挂到系统中。SharpDevelop中有插件树的思想,也就是每一个插件在系统中都有一个扩展点的路径。那么按照我最初对插件的理解,编写插件需要做的就是:
A、根据插件接口编写功能模块实现一个Command类
B、编写一个配置文件,指定Command类的扩展点(Extension)路径,挂到插件树中

之后按照这样的理解,我编写了一个察看插件树的插件AddinTreeView,打算挂到SharpDevelop中去。根据SharpDevelop对插件的定义,我把具体插件的AddinTreeViewCommand实现了之后,编写了一个配置文件AddinTreeView.addin如下:

AddIn name = "AddinTreeView"
author = "SimonLiu"
copyright = "GPL"
url = "http://www.icsharpcode.net"
description = "Display AddinTree"
version = "1.0.0"

Runtime
Import assembly="../../bin/ AddinTreeView.dll"/
/Runtime

Extension path = "/SharpDevelop/Workbench/MainMenu/Tools"
MenuItem id = "AddinTreeView"
label = "View AddinTree"
class = "Addins.AddinTreeView.AddinTreeViewCommand"/
/Extension
/AddIn



在配置文件中,Runtime节指定了插件功能模块所在的库文件Addins.dll的具体路径,在Extension节中指定了扩展点路径/SharpDevelop/Workbench/MainMenu/Tools(我是打算把它挂到主菜单的工具菜单下),然后在Extension内指定了它的Codon为 MenuItem以及具体的ID、标签、Command类名。这样做,SharpDevelop运行的很不错,我的插件出现在了Tools菜单下。之后,我又编写了一个SharpDevelop的资源管理器(ResourceEditor)的插件类ResourceEditor.dll并把它挂到Tool菜单下。同样的,我也写了一个ResourceEditor.addin文件来对应。系统工作的很正常。

如果我们对于每一个插件都编写这样的一个配置文件,那么插件的库文件(.dll)、插件配置文件(.addin)是一一对应的。不过这样就带来了一个小小的问题,在这样的一个以插件为基础的系统中,每一个菜单、工具栏按钮、窗体、面板都是一个插件,那么我们需要为每一个插件编写配置文件,这样就会有很多个配置文件(似乎有点太多了,不是很好管理)。SharpDevelop也想到了这个问题,于是它允许我们把多个插件的配置合并在一个插件的配置文件中。因此,我把我的两个插件库文件合并到一个Addins工程内生成了Addins.dll,又重新编写了我的插件配置文件MyAddins.addin如下:


AddIn name = "MyAddins"
author = "SimonLiu"
copyright = "GPL"
url = "http://www.icsharpcode.net"
description = "Display AddinTree"
version = "1.0.0"

Runtime
Import assembly="../../bin/Addins.dll"/
/Runtime

Extension path = "/SharpDevelop/Workbench/MainMenu/Tools"
MenuItem id = "ResourceEditor"
label = "Resource Editor"
class = "Addins.ResourceEditor.Command.ResourceEditorCommand"/
MenuItem id = "AddinTreeView"
label = "View AddinTree"
class = "Addins.AddinTreeView.AddinTreeViewCommand"/
/Extension
/AddIn



这样,我把两个插件的功能模块使用一个插件配置文件来进行配置。同样的,我也可以把几十个功能模块合并到一个插件配置文件中。SharpDevelop把这个插件配置文件称为“Addin(插件)”,而把具体的功能模块封装为Codon,使用Command类来包装具体的功能。SharpDevelop本身的核心配置SharpDevelopCore.addin里面就包含了所有的基本菜单、工具栏、PAD的插件配置。
我们回过头来看一下,现在我们有了两颗树。首先,插件树本身是一个树形的结构,这个树是根据系统所有插件的各个Codon的扩展点路径构造的,表示了各个Codon在插件树中的位置,各位看官可以通过我写的这个小小的AddinTreeView来看看SharpDevelop中实际的结构。其次,插件的配置文件本身也具有了一个树形的结构,这个树结构的根节点是系统的各个插件配置文件,其下是根据这个配置文件中的Extension节点的来构成的,描述了每个Extension节点下具有的Codon。我们可以通过SharpDevelop的Tools菜单下的AddinScout来看看这个树的结构。
我为了试验,把SharpDevelop的插件精简了很多,构成了一个简单的小插件系统。下面是这个精简系统的两个树的截图。各位看官可以通过这两副图理解一下插件树和插件配置文件的关系(只是看同样问题的两个角度,一个是Codon的ExtensionPath,一个是配置文件的内容)。


总结一下SharpDevelop插件的配置文件格式。首先是 AddIn节点,需要指定AddIn的名称、作者之类的属性。其次,在AddIn节点下的Runtime节点内,使用Import …来指定本插件配置中Codon所在的库文件。如果分布在多个库文件中,可以一一指明。然后,编写具体功能模块的配置。每个功能模块的配置都以扩展点Extension开始,指定了路径(Path)属性之后,在这个节点内配置在这个扩展点下具体的Codon。每个Codon根据具体不同的实现有不同的属性。各位看官可以研究一下SharpDevelop的核心配置文件SharpDevelopCore.addin的写法,相信很容易理解的。

2、插件系统的核心AddIn和AddInTree
前文讲到,在SharpDevelop的Main函数中,ServiceManager.Service在InitializeServicesSubsystem方法中首次调用了AddInTreeSingleton的AddInTree实例,AddinTree在这个时候进行了初始化。现在我们就来看看AddInTreeSingleton.AddInTree到底做了些什么事情,它定义在\src\Main\Core\AddIns\AddInTreeSingleton.cs文件中。

public static IAddInTree AddInTree
{
get
{
if (addInTree == null)
{
CreateAddInTree();
}
return addInTree;
}
}
AddInTreeSingleton是插件树的一个Singleton(具体的可以去看《设计模式》了),AddInTreeSingleton.AddInTree是一个属性,返回一个IAddinTree接口。这里我注意到一点,AddInTreeSingleton是从DefaultAddInTree继承下来的。既然它是一个单件模式,包含的方法全部都是静态方法,没有实例化的必要,而且外部是通过AddInTree属性来访问插件树,为什么要从DefaultAddInTree继承呢?这好像没有什么必要。这也许是重构过程中被遗漏的一个小问题吧。

我们先来看看IAddinTree接口的内容,它定义了这样的几个内容:
A、属性ConditionFactory ConditionFactory 返回一个构造条件的工厂类,这里的条件是指插件配置中的条件,我们以后再详细说明。
B、属性CodonFactory CodonFactory 返回一个构造Codon的工厂类。
C、属性AddInCollection AddIns 返回插件树的根节点Addin(插件)集合。
D、方法IAddInTreeNode GetTreeNode(string path) 根据扩展点路径(path)返回对应的树节点
E、方法void InsertAddIn(AddIn addIn) 根据AddIn中的扩展点路径添加一个插件到树中
F、方法void RemoveAddIn(AddIn addIn) 删除一个插件
G、方法Assembly LoadAssembly(string assemblyFile) 读入插件中Runtime节的Import指定的Assembly,并构造相应的CodonFactory和CodonFactory类。

AddInTreeSingleton在首次调用AddInTree的时候会调用CreateAddInTree方法来进行初始化。CreateAddInTree方法是这样实现的:


addInTree = new DefaultAddInTree();

初始化插件树为DefaultAddInTree的实例,这里我感受到了一点重构的痕迹。首先,DefaultAddInTree从名称上看是默认的插件树(既然是默认,那么换句话说还可以有其他的插件树)。但是SharpDevelop并没有给外部提供使用自定义插件树的接口(除非我们修改这里的代码),也就是说这个名称并不像它本身所暗示的那样。其次,按照Singleton通常的写法以及前面提到AddInTreeSingleton是从DefaultAddInTree继承下来的疑问,我猜想DefaultAddinTree的内容本来是在AddinTreeSingleton里面实现的,后来也许为了代码的条理性,把实现IAddinTree内容的代码剥离了出去,形成了DefaultAddinTree类。至于继承DefaultAddInTree的问题,也许这里本来是一个AddInTree的基类。这是题外话,也未加证实,各位看官可以不必放在心上(有兴趣的可以去找找以前SharpDevelop的老版本的代码来看看)。
这里有两个察看代码的线路,一个是DefaultAddInTree的构造函数的代码,在这个构造函数中构造了Codon和Condtion的工厂类。另外一个是CreateAddInTree后面的代码,搜索插件文件,并根据插件文件进行AddIn的构造。各位看官可以选择走分支线路,也可以选择先看主线(不过这样你会漏掉不少内容)。

2.1 支线 (DefaultAddInTree的构造函数)
我们把CreateAddInTree的代码中断一下压栈先,跳到DefaultAddInTree的构造函数中去看一看。DefaultAddInTree定义在\src\Main\Core\AddIns\DefaultAddInTree.cs文件中。在DefaultAddInTree的构造函数中,注意到它具有一个修饰符internal,也就是说这个类只允许Core这个程序集中的类对DefaultAddInTree进行实例化(真狠啊)。构造函数中的代码只有一句:

LoadCodonsAndConditions(Assembly.GetExecutingAssembly());
虽然只有一行代码,不过这里所包含的内容却很精巧,是全局的关键,要讲清楚我可有得写了。首先,通过全局的Assembly对象取得入口程序的Assembly,传入LoadCodonsAndConditions方法中。在该方法中,枚举传入的Assembly中的所有数据类型。如果不是抽象的,并且是AbstractCodon的子类,并且具有对应的CodonNameAttribute属性信息,那么就根据这个类的名称建立一个对应的CodonBuilder并它加入CodonFactory中(之后对Condition也进行了同样的操作,我们专注来看Codon部分,Condition跟Codon基本上是一样的)。
这里的CodonFactory类和CodonBuilder类构成了SharpDevelop插件系统灵活的基础,各位看官可要看仔细了。
我们以实例来演示,以前文我编写的AddinTreeViewCommand为例。在入口的Assembly中会搜索到MenuItemCodon,它是AbstractCodon的一个子类、包装MenuItem(菜单项)Command(命令)的Codon。符合条件,执行

codonFactory.AddCodonBuilder(new CodonBuilder(type.FullName, assembly));
首先根据类名MenuItemCodon和assembly 构造CodonBuilder。CodonBuilder定义在\src\Main\Core\AddIns\Codons\CodonBuilder.cs文件中。在CodonBuilder的构造函数中根据MenuItemCodon的CodonNameAttribute属性信息取得该Codon的名称MenuItem。CodonNameAttribute描述了Codon的名称,这个MenuItem也就是在.addin配置文件中对应的MenuItem标签,后文会看到它的重要用途。在CodonBuilder中除了包含了该Codon的ClassName(类名)和CodonName属性之外,就只有一个方法BuildCodon了。

public ICodon BuildCodon(AddIn addIn)
{
ICodon codon;
try {
// create instance (ignore case)
codon = (ICodon)assembly.CreateInstance(ClassName, true);

// set default values
codon.AddIn = addIn;
} catch (Exception) {
codon = null;
}
return codon;
}

很明显,BuildCodon根据构造函数中传入的assembly和类型的ClassName,建立了具体的Codon的实例,并和具体的AddIn关联起来。
之后,codonFactory调用AddCodonBuilder方法把这个CodonBuilder加入它的Builder集合中。我们向上一层,看看codonFactory如何使用这个CodonBuilder。
在文件\src\Main\Core\AddIns\Codons\CodonFactory.cs中,codonFactory只有两个方法。AddCodonBuilder方法把CodonBuilder加入一个以CodonName为索引的Hashtable中。另外一个方法很重要:

public ICodon CreateCodon(AddIn addIn, XmlNode codonNode)
{
CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder;

if (builder != null) {
return builder.BuildCodon(addIn);
}

throw new CodonNotFoundException(String.Format("no codon builder found for {0}", codonNode.Name));
}

在这里,addin是这个配置文件的描述(也就是插件),而这个XmlNode类型的CodonNode是什么东西?
还记得配置文件中在Extension标签下的Class、MenuItem、Pad之类的标签吗?我曾经说过,这些就是Codon的描述,现在我们来看看到底是不是如此。以前文的AddinTreeView配置为例:

Extension path = "/SharpDevelop/Workbench/MainMenu/Tools"
MenuItem id = "AddinTreeView"
label = "View AddinTree"
class = "Addins.AddinTreeView.AddinTreeViewCommand"/
/Extension

SharpDevelop在读入插件配置文件的Extension标签之后,就把它的ChildNodes(XmlElement的属性)依次传入CodonFactory的CreateCodon方法中。这里它的ChildNodes[0]就是这里的MenuItem id = ..... /节点,也就是codonNode参数了。这个XML节点的Name是MenuItem,因此CreateCodon的第一行

CodonBuilder builder = codonHashtable[codonNode.Name] as CodonBuilder;

根据节点的名称(MenuItem)查找对应的CodonBuilder。记得前面的CodonBuilder根据CodonNameAttribute取得了MenuItemCodon的CodonName吗?就是这个MenuItem了。CodonFactory找到了对应的MenuItemCodon的CodonBuilder(这个是在DefaultAddInTree的构造函数中调用LoadCodonsAndConditions方法建立并加入CodonFactory中的,还记得么?),之后使用这个CodonBuilder建立了对应的Codon,并把它返回给调用者。
就这样,通过CodonNameAttribute,SharpDevelop把addin配置文件的MenuItem节点、CodonBulder、MenuItemCodon三部分串起来形成了一个构造Codon的路线。

我们回过头来整理一下思路,SharpDevelop进行了下面这样几步工作:
A、建立各个Codon,使用CodonNameAttribute指明它在配置节点中的名称
B、DefaultAddInTree的构造函数中调用LoadCodonsAndConditions方法,搜索所有的Codon,根据Codon的CodonNameAttribute建立对应的CodonBuilder加入CodonFactory中。
C、读取配置文件,在Extension标签下遍历所有的节点,根据节点的Name使用CodonFactory建立对应的Codon。
其中,Codon的CodonNameAttribute、CodonBuilder的CodonName以及Extension标签下XML节点的Name是一致的。对于Condition(条件)的处理也是一样。
抱歉,我上网不是很方便也不太会在Blog里面贴图(都是为了省事的借口^o^),否则也许更好理解这里的脉络关系。

好了,看到这里,我们看看SharpDevelop中插件的灵活性是如何体现的。首先,addin配置中的Extension节点下的Codon节点名称并没有在代码中和具体的Codon类联系起来,而是通过CodonNameAttribute跟Codon联系起来。这样做的好处是,SharpDevelop的Codon和XML的标签一样具有无限的扩展能力。假设我们要自己定义一个Codon类SplashFormCodon作用是指定某个窗体作为系统启动时的封面窗体。要做的工作很简单:首先,在SplashFormCodon中使用CodonNameAttribute指定CodonName为Splash,并且在SplashFormCodon中定义自己需要的属性。然后,在addin配置文件使用Splash标签这样写:

Extension path = "/SharpDevelop/ "
Splash id = "MySplashForm" class = "MySplashFormClass"/
/Extension

是不是很简单?另外,对于Condition(条件)的处理也是一样,也就是说我们也可以使用类似的方法灵活的加入自己定义的条件。

这里我有个小小的疑问:不知道我对于设计模式的理解是不是有点小问题,我感觉CodonBuilder类的实现似乎并不如它的类名所暗示的是《设计模式》中的Builder模式,反而似乎应该是Proxy模式,因此我觉得改叫做CodonProxy是不是比较容易理解?各位看官觉得呢?
另外,虽然稍微麻烦了一小点,不过我觉得配置如果这样写会让我们比较容易和代码中具体的类关联起来:

Extension path = "/SharpDevelop/ "
Codon name=”Splash” id = "MySplashForm" class = "MySplashFormClass"/
/Extension

2.2 主线 (AddInTreeSingleton. CreateAddInTree)
啊~我写的有点累了。不过还是让我们继续AddInTreeSingleton中CreateAddInTree的代码。
在建立了DefaultAddInTree的实例后,AddInTreeSingleton在插件目录中搜索后缀为.addin的文件。还记得在SharpDevelop的Main函数中曾经调用过AddInTreeSingleton. SetAddInDirectories吗,就是搜索这个传入的目录。看来SharpDevelop把在插件目录中所有后缀为.addin的文件都看做是插件了。

FileUtilityService fileUtilityService = (FileUtilityService)ServiceManager.Services.GetService(typeof(FileUtilityService));
先学习一下如何从ServiceManager取得所需要的服务,在SharpDevelop中要取得一个服务全部都是通过这种方式取得的。调用GetService传入要获取的服务类的类型作为参数,返回一个IService接口,之后转换成需要的服务。

搜索插件目录找到一个addin文件后,调用InsertAddIns把这个addin文件中的配置加入到目录树中。

static StringCollection InsertAddIns(StringCollection addInFiles)
{
StringCollection retryList = new StringCollection();

foreach (string addInFile in addInFiles) {
AddIn addIn = new AddIn();
try {
addIn.Initialize(addInFile);
addInTree.InsertAddIn(addIn);
} catch (CodonNotFoundException) {
retryList.Add(addInFile);
} catch (ConditionNotFoundException) {
retryList.Add(addInFile);
} catch (Exception e) {
throw new AddInInitializeException(addInFile, e);
}
}

return retryList;
}

InsertAddIns建立一个对应的AddIn(插件),调用AddInTree的InsertAddIn方法把它挂到插件树中。在这里有一个小小的处理,由于是通过Assembly查找和插件配置中Codon的标签对应的类,而Codon类所在的Assembly是通过Import标签导入的。因此在查找配置中某个Codon标签对应的Codon类的时候,也许Codon类所在的文件是在其他的addin文件中Import的。这个时候在前面支线中讲到CodonFactory中查找CodonBuilder会失败,因此必须等到Codon类所在的addin处理之后才能正确的找到CodonBuilder。这是一个依赖关系的处理问题。
SharpDevelop在这里处理的比较简单,调用InsertAddIns方法的时候,凡是出现CodonNotFoundException的时候,都加入一个retryList列表中返回。在CreateAddinTree处理完所有的addin文件之后,再重新循环尝试处理retryList列表中的addin。如果某次循环中再也无法成功的加入retryList中的addin,那么才提示失败错误。

我们回头来看看对AddIn的处理。

2.2.1 addIn.Initialize (AddIn的初始化)
建立了AddIn的实例后,调用Initialize 方法进行初始化。AddIn是对一个.addin文件的封装,定义在\src\Main\Core\AddIns\AddIn.cs文件中。其中包含了.addin文件的根元素AddIn的描述,包括名称、作者、版权之类的属性。在AddIn节点下包括两种节点:一个是Runtime节点,包含了Import指定要导入的Assembly;另外一个是Extension节点,指定Codon的扩展点。在AddIn.Initialize方法中,使用XmlDocument对象来读取对应的addin文件。首先读取name、author 、copyright之类的基本属性,之后遍历所有的ChildNodes(子节点)。

如果子节点是Runtime节点,则调用AddRuntimeLibraries方法。

foreach (object o in el.ChildNodes)
{
XmlElement curEl = (XmlElement)o;

string assemblyName = curEl.Attributes["assembly"].InnerText;
string pathName = Path.IsPathRooted(assemblyName) ? assemblyName : fileUtilityService.GetDirectoryNameWithSeparator(path) + assemblyName;
Assembly asm = AddInTreeSingleton.AddInTree.LoadAssembly(pathName);
RuntimeLibraries[assemblyName] = asm;
}

通过AddInTreeSingleton.AddInTree.LoadAssembly方法把Assembly中所有的Codon和Condition的子类加入对应Factory类中(调用了LoadCodonsAndConditions方法,我们在DefaultAddInTree的构造函数中见过了),并且把该文件和对应的Assembly保存到RuntimeLibraries列表中。

如果子节点是Extension节点,则调用AddExtensions方法。

Extension e = new Extension(el.Attributes["path"].InnerText);
AddCodonsToExtension(e, el, new ConditionCollection());
extensions.Add(e);

根据这个扩展点的XML描述建立Extension对象加入到AddIn的Extensions列表中,并通过AddCodonsToExtension方法把其中包括的Codon加入到建立的Extension对象中。Extension对象是AddIn的一个内嵌类,其中一个重要的属性就是CodonCollection这个列表。AddCodonsToExtension就是把在配置中出现的Codon都加入到这个列表中保存。

来看看AddCodonsToExtension方法。在代码中我略过了对Condition(条件)的处理的分析和一些无关紧要的部分,我们把注意力集中在插件的处理。首先是一个 foreach (object o in el.ChildNodes) 遍历Extension下所有的子节点,对于每个子节点的处理如下:

XmlElement curEl = (XmlElement)o;
switch (curEl.Name)
{
(对条件的处理)
default:
ICodon codon = AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl);
AutoInitializeAttributes(codon, curEl);

(对codon.InsertAfter 和codon.InsertBefore 的处理,主要是处理codon在列表中的顺序问题,这一点在对于MenuItemCodon的处理上比较重要)

e.CodonCollection.Add(codon);
if (curEl.ChildNodes.Count 0)
{
Extension newExtension = new Extension(e.Path + '/' + codon.ID);
AddCodonsToExtension(newExtension, curEl, conditions);
extensions.Add(newExtension);
}
break;
}


我们看到了一个期待已久的调用

AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon(this, curEl);

经过了上文支线2.1代码中的铺垫,SharpDevelop使用建立好的CodonFactory,调用CreateCodon方法根据Extension下的节点构造出实际的Codon对象,一切尽在不言中了吧。
e.CodonCollection.Add(codon);把构造出来的Codon对象加入到Extension对象的CodonCollection列表中。
之后,在形如菜单的这种允许无限嵌套的结构中,SharpDevelop对此进行了处理。如果该节点有嵌套的子节点,那么构造一个新的Extension对象,递归调用AddCodonsToExtension添加到这个Extension对象中。注意一点,这个新构造的Extension对象并不是分开保存在Codon中,而是直接保存在AddIn的扩展点列表中。这样是为了方便查找,毕竟保存在具体的Codon中也没有什么用处,我们可以通过Extension对象的Path属性得知它在插件树中的具体位置。

2.2.2 addInTree.InsertAddIn(把AddIn添加到AddInTree中)
对AddIn的构造完成之后,需要把AddIn的实例对象添加AddInTree中管理。

addIns.Add(addIn);
foreach (AddIn.Extension extension in addIn.Extensions)
{
AddExtensions(extension);
}

在DefaultAddInTree中,保存了两课树。一个是根据插件文件的结构形成的树,每个插件文件作为根节点,往下依次是Extension、Codon节点。addIns.Add(addIn);就是把插件加入到这个树结构中。另外一个树是根据Extension的Path+Codon的ID作为路径构造出来的,每一个树节点是一个AddInTreeNode类,包含了在这个路径上的Codon对象。嵌套在这个节点中的Codon在通过它子节点来访问。在DefaultAddInTree中可以通过GetTreeNode来指定一个路径获得插件树上某一个节点的内容。
AddExtensions方法很简单,遍历Extension中所有的Codon,把Extension的Path+Codon的ID作为路径,创建这个路径上的所有节点,并把Codon连接到这个AddInTreeNode上。由于Codon的ID是全局唯一的,因此每一个AddInTreeNode都具有一个唯一的Codon。

3、最后一公里(Codon和Command的关联)
在插件树的讨论中,我们依次把AddIn-Extension-Codon的配置和他们对应的类关联了起来。不过我们一直没有涉及到Codon和它包含的Command是如何关联的。由于这个关联调用是在插件树外部的(记得在讲述SharpDevelop程序入口Main函数中,提到ServiceManager的方法InitializeServicesSubsystem么?AddServices((IService[])AddInTreeSingleton.AddInTree.GetTreeNode(servicesPath).BuildChildItems(this).ToArray(typeof(IService))); 这里就调用了BuildChildItems),因此单独在这里说明。实现这个关联的就是AddInTreeNode的BuildChildItems和BuildChildItem方法以及Codon的BuildItem方法。
BuildChildItem方法和BuildChildItems方法仅有一字之差,BuildChildItem是根据指定的Codon的ID在所属AddInTreeNode的子节点下查找包含该Codon的节点并调用该Codon的BuildItem方法;而BuildChildItems则是首先遍历所属AddInTreeNode的所有子节点,依次调用各个子节点的Codon的BuildItem方法,之后再调用所属AddInTreeNode的Codon的BuildItem方法(也就是一个树的后根遍历)。
重点在Codon的BuildItem方法。在AbstractCodon中,这个方法是一个抽象方法,SharpDevelop的代码注释中并没有明确说清楚这个方法是做什么用的。但是我们可以找一个Codon的实例来看看。例如ClassCodon的BuildItem:

public override object BuildItem(object owner, ArrayList subItems, ConditionCollection conditions)
{
System.Diagnostics.Debug.Assert(Class != null && Class.Length 0);
return AddIn.CreateObject(Class);
}
调用AddIn的CreateObject,传入Codon的Class(类名)作为参数,建立这个类的实例。例如这个配置

Extension path = "/Workspace/Autostart"
Class id = "InitializeWorkbenchCommand"
class = "ICSharpCode.SharpDevelop.Commands.InitializeWorkbenchCommand"/
/Extension
而Codon的中的Class(类名)属性就是ICSharpCode.SharpDevelop.Commands.InitializeWorkbenchCommand。也就是说,Codon的Class指的是实现具体功能模块的Command类的名称。在读取addin配置中的Runtime节的时候,AddInTree把Assembly保存到了RuntimeLibraries中,因此CreateObject方法可以通过它们来查找并建立类的实例。
各位看官可以再看看MenuItemCodon的实现,同样是建立了对应的SdMenuCommand。
这样,SharpDevelop本身的插件结构可以和具体的对象建立分离开来,实际的对象建立是在各个Codon的BuildItem中进行的。因此我们可以发现在SharpDevelop整个是基础插件系统部分没有任何GUI的操作,实现了很好的解耦效果。

4、问题
好了,本文对插件树构造的分析到此告一段落。我提一个小小的问题给各位看官思考:在构造插件树的过程中,如果Codon的某一个节点路径不存在(也就是说它的依赖项不存在),那么SharpDevelop会提示失败并且终止程序运行。可是实际上可能因为部署的原因或者权限的原因,某些Codon的失败并不会影响整个系统的使用,例如试用版本仅仅提供部分插件给客户使用,而并不希望系统因此而终止运行。那么就存在一个Codon依赖项失败而允许继续运行的问题。另外,我希望各个插件不在系统启动的时候全部调入系统,而是在运行期实际调用的时候才调入系统,也就是一个缓存机制,这样就可以实现系统插件的热部署。如何修改SharpDevelop的插件系统来实现这两个功能呢?

SharpDevelop的AddInTree View 插件
自从SharpDevelop 源码分析的系列文章发出来之后,很多朋友给了不错的评价,在这里先感谢各位朋友的鼓励。另外,评论中有位朋友想看看我在文章中提到的AddInTreeView插件,其实这个是个很简单的小东西,因此单独发在这里了。(好像没有找到那里能上传文件,因此直接贴代码了)

AddinTreeViewCommand.cs
/**//*
* Created by SharpDevelop.
* User: Administrator
* Date: 2004-10-4
* Time: 4:12
*
* To change this template use Tools | Options | Coding | Edit Standard Headers.
*/
using System;
using System.Windows.Forms;
using System.CodeDom.Compiler;

using ICSharpCode.SharpDevelop.Gui;
using ICSharpCode.SharpDevelop.Gui.Pads;
using ICSharpCode.Core.AddIns;
using ICSharpCode.Core.AddIns.Codons;
using ICSharpCode.SharpDevelop.Services;

namespace Addins.AddinTreeView
{
/**//// summary
/// Description of MyClass.
/// /summary
public class AddinTreeViewCommand: AbstractMenuCommand
{
public override void Run()
{
using (AddinTreeViewContent viewContent = new AddinTreeViewContent() )
{
WorkbenchSingleton.Workbench.ShowView(viewContent);
}
}
}

public class AddinTreeViewContent: AbstractViewContent
{
AddinTreeViewControl viewControl = new AddinTreeViewControl();

public override Control Control
{
get
{
return viewControl;
}
}

public override bool IsDirty
{
get
{
return false;
}
set
{
}
}

IWorkbenchWindow workbenchWindow;
public override IWorkbenchWindow WorkbenchWindow
{
get
{
return workbenchWindow;
}
set
{
workbenchWindow = value;
workbenchWindow.Title = "AddInTreeView";
}
}

public AddinTreeViewContent()
{
TitleName = "AddinTree View";
}


public override bool IsViewOnly
{
get
{
return true;
}
}
public void SaveFile(){}
public void Undo(){}
public void Redo(){}
public override void Save(){}
public override void Save(string filename){}
public override void Load(string filename)
{
}

public override string TabPageText
{
get
{
return "AddInTree";
}
}

}
}


AddinTreeViewControl.cs
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;

using ICSharpCode.SharpDevelop.Gui;
using ICSharpCode.Core.AddIns;
using ICSharpCode.Core.AddIns.Codons;

namespace Addins.AddinTreeView
{
/**//// summary
/// AddinTreeViewControl 的摘要说明。
/// /summary
public class AddinTreeViewControl : System.Windows.Forms.UserControl
{
private System.Windows.Forms.ColumnHeader chName;
private System.Windows.Forms.ListView lvAddin;
private System.Windows.Forms.ColumnHeader chInfo;
private System.Windows.Forms.CheckBox cbShowAddinInfo;
private System.Windows.Forms.Splitter splitter2;
private System.Windows.Forms.ListView lvDebug;
private System.Windows.Forms.Splitter splitter1;
private System.Windows.Forms.TreeView tvAddin;
private System.Windows.Forms.ColumnHeader chValue;
/**//// summary
/// 必需的设计器变量。
/// /summary
private System.ComponentModel.Container components = null;

public AddinTreeViewControl()
{
// 该调用是 Windows.Forms 窗体设计器所必需的。
InitializeComponent();

// TODO: 在 InitializeComponent 调用后添加任何初始化
InitAddinTreeView();
}

/**//// summary
/// 清理所有正在使用的资源。
/// /summary
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

组件设计器生成的代码#region 组件设计器生成的代码
/**//// summary
/// 设计器支持所需的方法 - 不要使用代码编辑器
/// 修改此方法的内容。
/// /summary
private void InitializeComponent()
{
this.chName = new System.Windows.Forms.ColumnHeader();
this.lvAddin = new System.Windows.Forms.ListView();
this.chValue = new System.Windows.Forms.ColumnHeader();
this.chInfo = new System.Windows.Forms.ColumnHeader();
this.cbShowAddinInfo = new System.Windows.Forms.CheckBox();
this.splitter2 = new System.Windows.Forms.Splitter();
this.lvDebug = new System.Windows.Forms.ListView();
this.splitter1 = new System.Windows.Forms.Splitter();
this.tvAddin = new System.Windows.Forms.TreeView();
this.SuspendLayout();
//
// chName
//
this.chName.Text = "属性";
this.chName.Width = 217;
//
// lvAddin
//
this.lvAddin.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.chName,
this.chValue});
this.lvAddin.Dock = System.Windows.Forms.DockStyle.Fill;
this.lvAddin.FullRowSelect = true;
this.lvAddin.GridLines = true;
this.lvAddin.HoverSelection = true;
this.lvAddin.Location = new System.Drawing.Point(443, 24);
this.lvAddin.MultiSelect = false;
this.lvAddin.Name = "lvAddin";
this.lvAddin.Size = new System.Drawing.Size(661, 509);
this.lvAddin.TabIndex = 13;
this.lvAddin.View = System.Windows.Forms.View.Details;
//
// chValue
//
this.chValue.Text = "值";
this.chValue.Width = 668;
//
// chInfo
//
this.chInfo.Text = "Info";
this.chInfo.Width = 673;
//
// cbShowAddinInfo
//
this.cbShowAddinInfo.Dock = System.Windows.Forms.DockStyle.Top;
this.cbShowAddinInfo.Location = new System.Drawing.Point(443, 0);
this.cbShowAddinInfo.Name = "cbShowAddinInfo";
this.cbShowAddinInfo.Size = new System.Drawing.Size(661, 24);
this.cbShowAddinInfo.TabIndex = 12;
this.cbShowAddinInfo.Text = "显示Codon隶属的Addin信息";
//
// splitter2
//
this.splitter2.Dock = System.Windows.Forms.DockStyle.Bottom;
this.splitter2.Location = new System.Drawing.Point(443, 533);
this.splitter2.Name = "splitter2";
this.splitter2.Size = new System.Drawing.Size(661, 3);
this.splitter2.TabIndex = 11;
this.splitter2.TabStop = false;
this.splitter2.Visible = false;
//
// lvDebug
//
this.lvDebug.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.chInfo});
this.lvDebug.Dock = System.Windows.Forms.DockStyle.Bottom;
this.lvDebug.Location = new System.Drawing.Point(443, 536);
this.lvDebug.Name = "lvDebug";
this.lvDebug.Size = new System.Drawing.Size(661, 216);
this.lvDebug.TabIndex = 10;
this.lvDebug.View = System.Windows.Forms.View.Details;
this.lvDebug.Visible = false;
//
// splitter1
//
this.splitter1.Location = new System.Drawing.Point(440, 0);
this.splitter1.Name = "splitter1";
this.splitter1.Size = new System.Drawing.Size(3, 752);
this.splitter1.TabIndex = 9;
this.splitter1.TabStop = false;
//
// tvAddin
//
this.tvAddin.Dock = System.Windows.Forms.DockStyle.Left;
this.tvAddin.FullRowSelect = true;
this.tvAddin.HideSelection = false;
this.tvAddin.ImageIndex = -1;
this.tvAddin.Location = new System.Drawing.Point(0, 0);
this.tvAddin.Name = "tvAddin";
this.tvAddin.SelectedImageIndex = -1;
this.tvAddin.Size = new System.Drawing.Size(440, 752);
this.tvAddin.TabIndex = 8;
this.tvAddin.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.tvAddin_AfterSelect);
//
// AddinTreeViewControl
//
this.Controls.Add(this.lvAddin);
this.Controls.Add(this.cbShowAddinInfo);
this.Controls.Add(this.splitter2);
this.Controls.Add(this.lvDebug);
this.Controls.Add(this.splitter1);
this.Controls.Add(this.tvAddin);
this.Name = "AddinTreeViewControl";
this.Size = new System.Drawing.Size(1104, 752);
this.ResumeLayout(false);

}
#endregion


void InitAddinTreeView()
{
TreeNode pathNode = tvAddin.Nodes.Add("AddinRoot");

tvAddin.BeginUpdate();
try
{
foreach ( AddIn addIn in AddInTreeSingleton.AddInTree.AddIns)
{
foreach ( ICSharpCode.Core.AddIns.AddIn.Extension e in addIn.Extensions)
{
string [] paths = e.Path.Split('/');
pathNode = tvAddin.Nodes[0];

for ( int i=0; ipaths.Length; i++)
{
bool foundPath = false;

if ( paths[i] == "" )
{
pathNode = tvAddin.Nodes[0];
continue;
}

for ( int j=0; jpathNode.Nodes.Count; j++)
{
if ( pathNode.Nodes[j].Text == paths[i] )
{
pathNode = pathNode.Nodes[j];
foundPath = true;
break;
}
}

if ( !foundPath )
{
pathNode = pathNode.Nodes.Add( paths[i] );
pathNode.Tag = new ArrayList();
//lvDebug.Items.Add("Add " + e.Path + " ---- " + paths[i]);
}
}

(pathNode.Tag as ArrayList).Add(e);
}
}
}
finally
{
tvAddin.EndUpdate();
}
}

void AddInfo(string Name, string Value)
{
lvAddin.Items.Add(Name).SubItems.Add(Value);
}

private void tvAddin_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e)
{
lvAddin.Items.Clear();

if ( e.Node.Tag != null )
{
foreach (AddIn.Extension et in (e.Node.Tag as ArrayList))
{
AddInfo("Extension", et.ToString());

foreach ( ICodon codon in et.CodonCollection)
{
AddInfo(" ┏ Codon ID", codon.ID);
AddInfo(" ┣ Codon Name", codon.Name);
AddInfo(" ┗ Codon Class", codon.Class);

if ( cbShowAddinInfo.Checked )
{
AddInfo(" ┣ Addin Name", codon.AddIn.Name);
AddInfo(" ┗ Addin FileName", codon.AddIn.FileName);

foreach ( ICSharpCode.Core.AddIns.AddIn.Extension ex in codon.AddIn.Extensions)
{
AddInfo(" ┣ Addin Extensions", ex.Path);
}
AddInfo(" ┗━━━━━━━━━", "");
}
}
}
}
}
}
}

# re: SharpDevelop的AddInTree View 插件 2006-02-03 13:05 古月春秋
现成的东西:
http://www.codeproject.com/csharp/ICSharpCodeCore.asp

Building Applications with the SharpDevelop Core

This article presents the AddIn architecture used in the IDE SharpDevelop [^], and how you can use it in your own Windows applications. The Addin infrastructure is LGPL-licensed, and so can be used in arbitrarily-licensed applications, ranging from GPL to commercial closed-source solutions. It has proven to scale for a 300 KLOC project such as SharpDevelop, so take a look at it to see if it fits your needs too 回复

This page is powered by Blogger. Isn't yours?