65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 SharpDevelop Core 构建应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (48投票s)

2006年1月3日

LGPL3

14分钟阅读

viewsIcon

301246

downloadIcon

3677

使用 XML 定义来构建应用程序,使其具有可扩展性。

引言

本文介绍了 IDE SharpDevelop 中使用的 AddIn 架构,以及如何将其用于您自己的 Windows 应用程序。Addin 基础架构是 LGPL 许可的,因此可以用于各种许可的应用程序,从 GPL 到商业闭源解决方案。它已证明可以扩展到像 SharpDevelop 这样 300 KLOC 的项目,所以请看看它是否适合您的需求。

大多数应用程序都使用某种形式的 AddIn 架构。但大多数时候,AddIns 只能执行一些特定的任务,例如扩展某个菜单或处理新的文件格式。

SharpDevelop AddIn 架构的目标是方便地在应用程序中提供可以扩展的“扩展点”。事实上,我们希望使其非常容易,以至于您可以一直使用它,从而允许 AddIns 扩展几乎任何内容。

在本文中,我们将构建一个小型文本编辑器应用程序。这是完成的应用程序的截图。

特点

AddIn 基础架构提供以下功能

  • AddIns 可以相互扩展。
  • AddIns 可以从多个位置加载。
  • AddIns 在首次需要时加载,以提高应用程序的启动时间。如果 AddIn 只添加菜单命令,它将在用户单击菜单项之前不会加载。
  • 包含启用+禁用、卸载和更新 AddIns 的基本功能。
  • 提供了一个图形化的 AddInManager,可以从“包文件”(参见截图)安装新的 AddIns。

它不能做什么

  • 提供一个具有预定义用户界面、面板、停靠、文件管理等的“应用程序平台”,但可以在其之上构建(正如我们在 SharpDevelop 中所做的那样)。
  • 它不使用 AppDomains 来加载 AddIns,所有内容都放入主 AppDomain,因此卸载或禁用 AddIns 需要重新启动应用程序。

基于核心的应用程序组件

让我们从查看示例应用程序的程序集开始。

核心使用 log4net 进行日志记录(在 LoggingService 类中)。如果您想使用不同的日志记录引擎,只需修改 LoggingService.cs 文件即可。

AddIns 必须引用 ICSharpCode.Core,并且还必须引用 Base(除非 AddIns 不需要与主机应用程序交互)。AddIns 可以相互引用,也可以附带额外的库。

核心

核心负责加载 AddIns 并存储扩展点列表以及扩展它们的 AddIns。扩展点存储在一个称为 AddIn 树的树状结构中。

此外,ICSharpCode.Core 包含用于以下方面的代码:

  • 保存/加载设置,
  • 日志记录,
  • 向用户显示消息,
  • 读取(可本地化的)资源,
  • 创建可由 AddIns 扩展的菜单和工具栏。

Base 中有什么?

Base 是应用程序的基础 AddIn。对于 Core 来说,它只是一个普通的 AddIn,但它提供了所有基本功能,因此所有其他 AddIns 都需要引用它。

Base 包含控制应用程序主窗口的代码,以及文件管理、撤销/重做以及可能的面板(可停靠面板)等主操作的接口。

在本文的示例应用程序中,Base 包含一个类似记事本的应用程序。下载包含两个 AddIns – AddInManager 和 RichTextEditor。AddInManager 允许用户安装打包的 AddIns。有关更多信息,请观看 AddIn Manager 视频教程 [^]

AddIn 树

编译后的 AddIns 由两个(或更多)文件组成:AddIn XML 定义(.addin 文件)、AddIn 库(.dll)以及可能的其他文件或库。所有 AddIns 的 XML 定义在应用程序启动时读取,并合并成一个单一的树状结构:AddIn 树。

AddIn 树的结构类似于文件系统。例如,如果我们想访问 SubNode2,我们必须指定路径为 /Path1/SubPath1/Node1/SubNode2

路径代表应用程序的一个扩展点。节点是 AddIn(或基础应用程序)添加到扩展点的某个行为。节点可以有子节点,如本示例路径所示。

AddIn 树最常见的用途是扩展菜单和工具栏。当应用程序的某个部分想要创建菜单或工具栏时,它会使用 AddIn 树中的路径。对于 SharpDevelop,路径 "/SharpDevelop/MainMenu" 包含主菜单项,路径 "/SharpDevelop/Browser/Toolbar" 包含浏览器工具栏(在 SharpDevelop 中,浏览器用于起始页和集成帮助)。

那么,如何加载这样的工具栏呢?这要归功于核心提供的 ToolbarService。

toolStrip = ToolbarService.CreateToolStrip(this, 
                         "/SharpDevelop/Browser/Toolbar");
toolStrip.GripStyle = ToolStripGripStyle.Hidden;
this.Controls.Add(toolStrip);

正如你所见,创建可扩展的工具栏非常容易。

这个工具栏是从哪里加载的?当然,是从 AddIn 文件中该路径的 XML 定义中加载的。

<Path name = "/SharpDevelop/Browser/Toolbar">
  <ToolbarItem id      = "Back"
               icon    = "Icons.16x16.BrowserBefore"
               tooltip = "${res:AddIns.HtmlHelp2.Back}"
               class   = " SharpDevelop.BrowserDisplayBinding.GoBack"/>
  <ToolbarItem id      = "Forward"
               icon    = "Icons.16x16.BrowserAfter"
               tooltip = "${res:AddIns.HtmlHelp2.Forward}"
               class   = " SharpDevelop.BrowserDisplayBinding.GoForward"/>
  [...]
  <ToolbarItem id = "Separator1" type  = "Separator"/>
  <ToolbarItem id      = "GoHome"
               icon    = "Icons.16x16.BrowserHome"
               tooltip = "${res:AddIns.HtmlHelp2.Homepage}"
               class   = "SharpDevelop.BrowserDisplayBinding.GoHome"/>
  [...]

这段 XML 定义了路径 "/SharpDevelop/Browser/Toolbar"。它包含子节点 "/SharpDevelop/Browser/Toolbar/Back" 等。每个节点都有一个关联的CodonCodon 是 AddIn 树节点的内存表示。加载 AddIn 树时,会创建一个 Codon 类的实例。其 name 属性设置为 "ToolbarItem",其 ID 属性设置为 "Back"。其他属性被放入一个 "Properties" 容器中(工作方式类似于 Hashtable)。

icon 属性引用存储在 ResourceService 中的图像;tooltip 属性使用 "StringParser" 服务解析以插入本地化字符串。class 是处理命令的类的完全限定名。它必须实现 ICommand 接口。但这些只是 "ToolbarItem" 的特殊情况,您可以使用 AddInTree 来存储任何信息。

关于 AddIn 树的重要事实是*它是由所有 AddIns 的 AddIn 定义组合而成的*。例如,帮助 AddIn 的 HtmlHelp2.addin 文件包含以下内容。

<Path name = "/SharpDevelop/Browser/Toolbar">
  <Condition name = "BrowserLocation" urlRegex = "^ms-help:">
    <ToolbarItem id      = "SyncHelpTopic"
                 icon    = "Icons.16x16.ArrowLeftRight"
                 tooltip = "${res:AddIns.HtmlHelp2.SyncTOC}"
                 class   = "HtmlHelp2.SyncTocCommand"
                 insertafter = "Separator1"/>
    [...]

您可以看到 AddIns 可以将新项添加到现有路径中,并使用特殊的属性 insertafterinsertbefore 来指定插入元素的位置。

您还可以看到 Codons 可以分配条件;我将在后面的文章中(详细)解释条件。

核心支持以下 Codon 名称:

通过 System.Reflection 调用类型无参数构造函数来创建对象实例。

FileFilter

OpenFileDialogSaveFileDialog 创建文件过滤器条目。

Include

包含 addin 树中另一个位置的一个或多个项目。您可以使用属性 "item"(包含单个项目)*或*属性 "path"(包含目标路径的所有项目)。

图标

用于创建文件类型和图标之间的关联。

MenuItem

为菜单创建一个 System.Windows.Forms.ToolStrip* 项目。

ToolbarItem

为工具栏创建一个 System.Windows.Forms.ToolStrip* 项目。

当然,AddIns(或您的基础项目)可以通过添加自定义 doozers 来为其他数据创建新的元素类型。Doozers 是从 Codons 创建对象的类。ICSharpCode.Core 包含表中提到的 Codon 类型的 doozer 类。自定义 doozers 将在另一篇文章中介绍。

但是,在大多数情况下,使用 Class 就足够了。它允许您将任何对象放入 AddIn 树中。如果放入路径的所有类都实现了特定的接口(例如 IMyInterface),您可以使用

foreach (IMyInterface obj in AddInTree.BuildItems(
                           "/Path/SubPath", this, false)) {
  // the third parameter means that no exception should 
  // be thrown if the path doesn’t exist
  obj.SomeMethod(…);
}

这使得 AddInTree 可以用来定义文件类型处理程序或一组在某个操作上运行的命令。

如果 AddIn 需要在应用程序启动时运行操作,“/Workspace/Autostart” 是一个预定义的路径,在核心初始化时(加载 AddIn 树后立即)运行,其中存储的对象必须实现 ICommand

示例应用程序

现在让我们回到我们的示例应用程序,那个小的文本编辑器。主窗体称为“Workbench”,显示主菜单、工具栏和 ViewContent。ViewContent 可以是任何可以像文档一样行为的东西。我们的示例应用程序一次只能显示一个 ViewContent。

“Edit”和“Format”菜单被故意省略:在下一篇文章中,我们将把它们添加为 AddIn。

该应用程序仅用于演示您可以使用 ICSharpCode.Core 做什么;它不能用作功能齐全的文本编辑器,因为它不支持编码(仅 UTF-8)。作为一个示例 AddIn,下载包含一个“RichTextEditor”AddIn,它支持简单的富文本编辑。

我们将在以下步骤中构建此应用程序

  1. 设置核心所需的启动代码。
  2. 设置应用程序窗口所需的代码。
  3. 使用 PropertyService 加载和保存应用程序设置。
  4. 实现菜单命令。
  5. 使用可扩展的“显示绑定”打开文件。
  6. 使用本地化资源。

为了让您更好地了解项目,这是项目浏览器的截图。

启动

让我们看看启动代码(“Startup”项目中的文件 Start.cs,方法 Start.Main)以及其中使用的 ICSharpCode.Core 的功能。

// The LoggingService is a small wrapper around log4net.
// Our application contains a .config file telling log4net to write
// to System.Diagnostics.Trace.
LoggingService.Info("Application start");

// Get a reference to the entry assembly (Startup.exe)
Assembly exe = typeof(Start).Assembly;

// Set the root path of our application. 
// ICSharpCode.Core looks for some other
// paths relative to the application root:
// "data/resources" for language resources, 
// "data/options" for default options
FileUtility.ApplicationRootPath = Path.GetDirectoryName(exe.Location);

LoggingService.Info("Starting core services...");

// CoreStartup is a helper class 
// making starting the Core easier.
// The parameter is used as the application 
// name, e.g. for the default title of
// MessageService.ShowMessage() calls.
CoreStartup coreStartup = new CoreStartup("Test application");
// It is also used as default storage 
// location for the application settings:
// "%Application Data%\%Application Name%", but you 
// can override that by setting c.ConfigDirectory

// Specify the name of the application settings 
// file (.xml is automatically appended)
coreStartup.PropertiesName = "AppProperties";

// Initializes the Core services 
// (ResourceService, PropertyService, etc.)
coreStartup.StartCoreServices();

// Registeres the default (English) strings 
// and images. They are compiled as
// "EmbeddedResource" into Startup.exe.
// Localized strings are automatically 
// picked up when they are put into the
// "data/resources" directory.
ResourceService.RegisterNeutralStrings(
  new ResourceManager("Startup.StringResources", exe));
ResourceService.RegisterNeutralImages(
  new ResourceManager("Startup.ImageResources", exe));

LoggingService.Info("Looking for AddIns...");
// Searches for ".addin" files in the 
// application directory.
coreStartup.AddAddInsFromDirectory(
  Path.Combine(FileUtility.ApplicationRootPath, "AddIns"));

// Searches for a "AddIns.xml" in the user 
// profile that specifies the names of the
// AddIns that were deactivated by the 
// user, and adds "external" AddIns.
coreStartup.ConfigureExternalAddIns(
  Path.Combine(PropertyService.ConfigDirectory, "AddIns.xml"));

// Searches for AddIns installed by the 
// user into his profile directory. This also
// performs the job of installing, 
// uninstalling or upgrading AddIns if the user
// requested it the last time this application was running.
coreStartup.ConfigureUserAddIns(
  Path.Combine(PropertyService.ConfigDirectory, "AddInInstallTemp"),
           Path.Combine(PropertyService.ConfigDirectory, "AddIns"));

LoggingService.Info("Loading AddInTree...");
// Now finally initialize the application. 
// This parses the ".addin" files and
// creates the AddIn tree. It also 
// automatically runs the commands in
// "/Workspace/Autostart"
coreStartup.RunInitialization();

LoggingService.Info("Initializing Workbench...");
// Workbench is our class from the base 
// project, this method creates an instance
// of the main form.
Workbench.InitializeWorkbench();

try {
  LoggingService.Info("Running application...");
  // Workbench.Instance is the instance of 
  // the main form, run the message loop.
  Application.Run(Workbench.Instance);
} finally {
  try {
    // Save changed properties
    PropertyService.Save();
  } catch (Exception ex) {
    MessageService.ShowError(ex, "Error storing properties");
} }
LoggingService.Info("Application shutdown");

Workbench 初始化

“Base”项目中的 Workbench 类是我们应用程序的主窗口。在其构造函数(由 Workbench.InitializeWorkbench 调用)中,它使用 MenuServiceToolbarService 来创建主窗口的内容。

// restore form location from last session
FormLocationHelper.Apply(this, "StartupFormPosition");

contentPanel = new Panel();
contentPanel.Dock = DockStyle.Fill;
this.Controls.Add(contentPanel);

menu = new MenuStrip();
MenuService.AddItemsToMenu(menu.Items, 
             this, "/Workbench/MainMenu");

toolbar = ToolbarService.CreateToolStrip(this, 
                          "/Workbench/Toolbar");

this.Controls.Add(toolbar);
this.Controls.Add(menu);

// Start with an empty text file
ShowContent(new TextViewContent());

// Use the Idle event to update the 
// status of menu and toolbar items.
Application.Idle += OnApplicationIdle;

FormLocationHelper 不是由 Core 提供,而是由“Base”项目中的一个辅助类提供。它使用 PropertyService 来加载和存储主窗口的位置。

PropertyService

Core 包含一个名为“PropertyService”的类,可用于存储应用程序设置。看看用于保存和恢复 Form 位置的代码,以便了解它的易用性。

public static void Apply(Form form, string propertyName)
{
  form.StartPosition = FormStartPosition.Manual;
  form.Bounds = Validate(
    PropertyService.Get(propertyName, GetDefaultBounds(form)));
  form.Closing += delegate {
    PropertyService.Set(propertyName, form.Bounds);
  };
}

PropertyServiceGetSet 方法是泛型方法。

public static T Get<T>(string property, T defaultValue)
public static void Set<T>(string property, T value)

C# 编译器从 GetDefaultBounds 推断出类型,它只是返回居中于活动屏幕的 Form 的边界,并读取属性。Validate 方法确保位置有效;我们不希望在不再存在的辅助监视器上显示 Form。当 Form 关闭时,会保存新位置。PropertyService 支持提供 TypeConverter 的类型,因此您可以将其用于 .NET 的大多数内置类型,并且添加对自定义类型的支持也很容易。此外,PropertyService 支持存储一维数组(如果数组元素类型具有 TypeConverter)。

菜单命令

您已经看到菜单命令在 .addin 文件中声明。以下是针对我们的文本编辑器应用程序的命令:

<Path name = "/Workbench/MainMenu">
    <MenuItem id = "File"
             type = "Menu"
             label = "${res:Demo.Menu.File}">
        <MenuItem id = "New"
                 label = "&New"
                 shortcut = "Control|N"
                 icon = "Icons.New"
                 class = "Base.NewFileCommand"/>

现在看看 NewFileCommand 类(显然不是由核心本身提供的)。

public class NewFileCommand : AbstractMenuCommand
{
    public override void Run()
    {
        Workbench workbench = (Workbench)this.Owner;
        if (workbench.CloseCurrentContent()) {
                workbench.ShowContent(new TextViewContent());
}   }   }

在创建菜单或工具栏时,“Owner” workbench 会自动传递。

ToolbarService.CreateToolStrip(this, "/Workbench/Toolbar");

第一个参数是工具栏的所有者,为工具栏创建的所有命令都会将其 Owner 属性设置为创建工具栏时传递的所有者。这在创建项目的上下文菜单时很有用。

打开文件

现在继续打开现有文件。我们不知道用户将尝试打开什么类型的文件,并且我们希望给 AddIn 作者添加更多文件类型支持的可能性。因此,OpenFileDialog 中使用的文件过滤器必须是可扩展的,AddIns 应该能够为用户选择的文件创建自定义视图内容。

using (OpenFileDialog dlg = new OpenFileDialog()) {
    dlg.CheckFileExists = true;
    dlg.DefaultExt = ".txt";
    dlg.Filter = FileViewContent.GetFileFilter("/Workspace/FileFilter");
    if (dlg.ShowDialog() == DialogResult.OK) {
        IViewContent content = 
          DisplayBindingManager.CreateViewContent(dlg.FileName);
        if (content != null) {
            workbench.ShowContent(content);
}   }   }

首先看看文件过滤器是如何构造的。

<Path name = "/Workspace/FileFilter">
<FileFilter id = "Text" name = "Text files" extensions = "*.txt"/>
<FileFilter id = "LogFiles" name = "Log files" extensions = "*.log"/>
</Path>

然后是 GetFileFilter 方法。

public static string GetFileFilter(string addInTreePath)
{
    StringBuilder b = new StringBuilder();
    b.Append("All known file types|");
    foreach (
     string filter in AddInTree.BuildItems(addInTreePath, null, true)) {
        b.Append(filter.Substring(filter.IndexOf('|') + 1));
        b.Append(';');
    }
    foreach (
     string filter in AddInTree.BuildItems(addInTreePath, null, true)) {
        b.Append('|');
        b.Append(filter);
    }
    b.Append("|All files|*.*");
    return b.ToString();
}

如您所见,BuildItems 方法在这种情况下返回一个 ArrayList 字符串。对于文件过滤器,我们不需要任何“所有者”,这就是为什么 BuildItems 的第二个参数为 null

FileFilter doozer 以“name|extensions”的格式返回字符串;这用于连接完整的过滤器字符串。

现在我们来看看视图内容的创建。正如“功能”部分所说,Core 没有为您提供此任务的预定义类,但 DisplayBindingManager 很容易编写。

我们将定义一个新的接口 IDisplayBinding 和 AddIn 树中的一个新路径。AddIns 将能够使用 <Class> 元素将实现该接口的类的实例添加到 AddIn 树中。我们的 DisplayBindingManager 构建这些对象,并要求每个对象创建一个文件视图内容。第一个能够打开文件的对象将被使用。

实现此行为很容易。

/// <summary>
/// Interface for classes that are able to open a file 
/// and create a <see cref="IViewContent"/> for it.
/// </summary>
public interface IDisplayBinding
{
    /// <summary>
    /// Loads the file and opens a <see cref="IViewContent"/>.
    /// When this method returns <c>null</c>, 
    /// the display binding cannot handle the file type.
    /// </summary>
    IViewContent OpenFile(string fileName);
}

public static class DisplayBindingManager
{
    static ArrayList items;

    public static IViewContent CreateViewContent(
                                  string fileName)
    {
        if (items == null) {
            items = AddInTree.BuildItems(
                     "/Workspace/DisplayBindings", null, true);
        }
        foreach (IDisplayBinding binding in items) {
            IViewContent content = binding.OpenFile(fileName);
            if (content != null) {
                return content;
            }
        }
        return null;
}   }

正如您所见,我们只是从 AddIn 树构建所有 DisplayBinding 类。第一个能够打开文件的显示绑定将被使用。

来自“Base”项目的 AddIn 定义 Base.addin 试图将所有内容都作为文本文件打开。我们的“RichTextEditor”示例 AddIn 必须使用“insertbefore”以确保它首先被使用。

<Path name = "/Workspace/DisplayBindings">
    <Class id = "RTF"
           class = "RichTextEditor.DisplayBinding"
           insertbefore = "Text"/>
</Path>

如果我们不使用“insertbefore”,基础文本编辑器将显示富文本源代码,而我们的富文本编辑器将永远不会被要求打开文件。

这是显示绑定类的代码。

public class DisplayBinding : IDisplayBinding
{
    public IViewContent OpenFile(string fileName)
    {
        if (Path.GetExtension(fileName).ToLowerInvariant() == ".rtf") {
            return new RichTextViewContent(fileName);
        }
        return null;
}   }

这种方法将导致所有显示绑定 AddIns 在首次打开文件时被加载;在以后的文章中,我将通过将文件扩展名检查移到 XML 中来向您展示一种更好的方法。

包含来自其他 AddIn 树路径的项目

<Include>.addin XML 声明的一个非常有用的元素。

<Path name = "/Workbench/MainMenu">
  <MenuItem id = "Tools"
            type = "Menu"
            label = "&Tools">
    <Include id = "ToolList" path = "/Workspace/Tools"/>
  </MenuItem>
</Path>

这将把 /Workspace/Tools 中的所有元素插入到 Include 节点的位置。 /Workspace/Tools 是一种“标准化”路径,用于与基础应用程序没有紧密耦合的 AddIns,只需在“Tools”菜单中调用时打开一个新窗口。下载中包含的 AddInManager AddIn 使用此路径,因此您可以启动 AddInManager 并使用它来禁用和启用 RTF 集成。

资源

核心通过资源文件支持多语言本地化。ResourceService 从多个位置读取资源。

  • 主要的英文 StringResources 文件通常嵌入到 Startup 应用程序中。它通过以下方式注册:
ResourceService.RegisterNeutralStrings(
    new ResourceManager("Startup.StringResources", assembly));
  • ResourceService 自动从 data/resources 目录(相对于应用程序根目录)读取特定语言的 .resources 文件。这是为应用程序提供本地化字符串的常用方法。

但是,AddIns 也可能提供自己的本地化字符串资源。例如,AddInManager(下载中包含)带有英语和德语资源文件。两者都被设置为“EmbeddedResource”,因此英语资源包含在 AddIn 程序集中,而德语资源则放入一个卫星程序集中。

当 AddInManager 启动时,它会调用:

ResourceService.RegisterStrings(
    "ICSharpCode.AddInManager.StringResources", 
                               typeof(ManagerForm).Assembly);

ResourceService 将从 AddIn 程序集中加载字符串资源,并查找当前语言的卫星程序集。ResourceService.GetString() 将探测所有已注册的资源并返回当前语言的字符串。

但是,我们不能直接在 XML 文件中调用方法,所以我们必须使用其他方法。

StringParser

StringParser 是核心中的一个 static 类,用于扩展 "{xyz}" 样式的属性值。StringParser 用于 AddIn 树中所有菜单项的标签,因此您可以使用它来包含翻译的字符串或其他变量。

<MenuItem id = "File" type = "Menu" label = "${res:Demo.Menu.File}">

您可以使用 ${res:ResourceName}ResourceService 包含字符串。您还可以使用 ${property:PropertyName}PropertyService 包含值,或 ${env:VariableName} 包含环境变量。可以通过 StringParser.Properties 设置其他变量。此外,您可以使用 PropertyService.PropertyObject 注册新的前缀。属性对象可以是任何对象 - 使用 Reflection 访问成员。${exe:PropertyName} 可用于访问入口程序集的 FileVersionInfo 对象的任何属性,例如 ${exe:FileVersion}

许可证

ICSharpCode.CoreAddInManager 在 GNU Lesser General Public License 条款下许可。简而言之,您可以在商业项目中使用这些库,但您必须发布对这些库所做的任何修改的源代码。

本文中的示例核心“Base”和“Startup”可以自由使用,它遵循 BSD 许可。

摘要

本文向您展示了如何为您的应用程序使用 ICSharpCode.Core。我们讨论了核心提供的服务,并展示了您的应用程序需要实现的内容。我们使用 AddInTree 来存储菜单项、工具栏项、文件过滤器条目和我们自己的自定义对象。惰性加载、自定义 doozers 和条件将在下一篇文章中解释。

历史

  • 2005 年 1 月 3 日:文章发布(基于 SharpDevelop 2.0.0.962)。
© . All rights reserved.