通过SharpDevelop界面接口定义的分析来体会如何设计更易扩展的应用程序,同时也是对SharpDevelop插件应用的又一次深切体验...
SharpDevelop浅析_2_User Interface
创建易扩展且功能模块松散耦合的应用程序
Demo界面及功能解释
相关概念
Demo代码分析
总结
Demo下载
1、Demo界面及功能解释:
启动界面:
支持打开图片:
支持打开网页:
功能说明:程序有个浮动窗口显示当前磁盘文件,选择文件并双击后在中心窗口中以适当的察看器打开文件;持拖曳打开文件;程序关闭后再次启动时能够“记忆”前一次程序关闭时的窗体大小位置及打开过的文件。
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<Path name = "/SharpDevelop/Workbench/Pads">
3 <!--
4 ProjectBrowser
5 ClassBrowser
6 SideBar
7 ErrorList
8 TaskList
9 CompilerMessageView
10 PropertyPad
11 SearchResults
12 Bookmarks
13 DefinitionView
14
15 -->
16 <Pad id = "FileScout"
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</Path>
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<Path name = "/SharpDevelop/Workbench/DisplayBindings">
2 <DisplayBinding id = "Browser"
3 supportedformats = "Web Pages"
4 class = "SDUserInterface.GUI.ViewContent.BrowserDisplayBinding"/>
5 <DisplayBinding id = "Text"
6 insertafter = "Browser"
7 supportedformats = "Text Files,Source Files"
8 class = "SDUserInterface.GUI.ViewContent.TextViewDisplayBinding" />
9 <DisplayBinding id = "Image"
10 insertbefore = "Text"
11 supportedformats = "图片"
12 class = "SDUserInterface.GUI.ViewContent.ImageDisplayBinding" />
13</Path>
值得注意的是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<IViewContent> ViewContentCollection { get; }
6
7 List<PadDescriptor> 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精简和突出重点。