Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building Applications with the SharpDevelop Core

0.00/5 (No votes)
3 Jan 2006 15  
Use XML definitions for your applications to make them extensible.

Introduction

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.

Most of the applications use some sort of AddIn architecture. But most of the time AddIns are limited to do only a few specific jobs - like extending a certain menu or handling a new file format.

The goal of the SharpDevelop AddIn architecture is to make it easy to provide "extension points" in the application that can be extended. In fact, we wanted to make it so easy that you could use it all the time, thus allowing AddIns to extend nearly everything.

In this article, we'll build a small text editor application. Here is a screenshot of the finished application:

Features

The AddIn infrastructure provides the following functionalities:

  • AddIns can extend each other.
  • AddIns can be loaded from multiple locations.
  • AddIns are loaded when they are needed the first time to improve the startup time of the application. If an AddIn only adds menu commands, it is not loaded until the user clicks on the menu item.
  • Contains the basic functionality for enabling + disabling, uninstalling and updating AddIns.
  • A graphical AddInManager is available as AddIn to install new AddIns from "package files" (see screenshot).

What it doesn�t do:

  • Provide an "application platform" with predefined user-interface, pads, docking, file management etc., but it is possible to build that on top of the core (as we have done with SharpDevelop).
  • It does not use AppDomains to load the AddIns, everything is put into the main AppDomain, so uninstalling or disabling AddIns requires a restart of the application.

Components in an application based on the core

Let�s start with a view on the assemblies of our sample application:

The core uses log4net for logging (in the class LoggingService). If you want to use a different logging engine, you only need to modify the file LoggingService.cs.

AddIns must reference ICSharpCode.Core, and also reference Base (except for AddIns that don't need to interact with the host application). AddIns can reference each other and they can also come with additional libraries.

The core

The core is responsible for loading the AddIns and storing a list of extension points and the AddIns extending them. The extension points are stored in a tree structure called AddIn tree.

Moreover, ICSharpCode.Core contains code for:

  • Saving/loading settings,
  • Logging,
  • Showing messages to the user,
  • Reading (localizable) resources,
  • Creating menus and toolbars that can be extended by AddIns.

What�s in the Base?

Base is the base AddIn of the application. To the Core, it's just a normal AddIn, but it provides all the essential functionalities so all other AddIns need references to it.

Base contains the code that controls the main window of the application, the interfaces for the main actions like file management, undo + redo and possibly pads (dockable panels).

In this article's example application, Base contains a Notepad-like application. The download includes two AddIns � AddInManager and RichTextEditor. AddInManager allows the user to install packaged AddIns. For more information, watch the AddIn Manager Video Tutorial [^].

The AddIn tree

Compiled AddIns consist of two (or more) files: the AddIn XML definition (.addin file), the AddIn library (.dll) and maybe additional files or libraries. The XML definitions of all AddIns are read when the application is started and combined into a single tree-structure: the AddIn tree.

The AddIn tree is structured like a file system. For example, if we want to access SubNode2, we have to specify the location as /Path1/SubPath1/Node1/SubNode2.

A path represents an extension point of the application. A node is some behavior added to the extension point by an AddIn (or the base application). Nodes can have sub-nodes, as presented in this example path.

The most common use of the AddIn tree is to extend menus and tool bars. When some part of the application wants to create a menu or toolbar, it uses a path in the AddIn tree. In the case of SharpDevelop, the path "/SharpDevelop/MainMenu" contains the items of the main menu, the path "/SharpDevelop/Browser/Toolbar" contains the toolbar of the browser (in SharpDevelop, the browser is used for the start page and the integrated help).

So, how do you load such a toolbar? Thanks to the ToolbarService provided by the core:

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

As you can see, creating extensible toolbars is extremely easy.

Where did this toolbar get loaded from? Of course, from an XML definition of that path in the AddIn file:

<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"/>
  [...]

This piece of XML defines the path "/SharpDevelop/Browser/Toolbar". It contains sub-nodes "/SharpDevelop/Browser/Toolbar/Back" etc. Every node has a Codon associated to it. A Codon is the in-memory representation of an AddIn tree node. When the AddIn tree is loaded, an instance of the Codon class is created. Its name property is set to "ToolbarItem", its ID property to "Back". The other attributes are put into a "Properties" container (works like a Hashtable).

The icon attribute refers to an image stored in the ResourceService; the tooltip attribute is parsed using the "StringParser" service to insert localized strings. class is the fully qualified name of the class handling the command. It has to implement the interface ICommand. But these are just the special cases for the "ToolbarItem", you can use the AddInTree to store any information.

The important fact about the AddIn tree is that it is constructed by combining the AddIn definitions from all AddIns. For example, the HtmlHelp2.addin file from the help AddIn contains this:

<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"/>
    [...]

You can see that AddIns can add new items into the existing paths and use the special attributes insertafter and insertbefore to specify the location of the inserted elements.

You can also see that Codons can have conditions assigned to them; I'll explain the conditions (in detail) in a later article.

The following Codon names are supported by the core:

Class

Creates object instances by invocating a type's parameterless constructor via System.Reflection.

FileFilter

Creates file filter entries for the OpenFileDialog or SaveFileDialog.

Include

Includes one or multiple items from another location in the addin tree. You can use the attribute "item" (to include a single item) or the attribute "path" (to include all items from the target path).

Icon

Used to create associations between file types and icons.

MenuItem

Creates a System.Windows.Forms.ToolStrip* item for use in a menu.

ToolbarItem

Creates a System.Windows.Forms.ToolStrip* item for use in a toolbar.

Of course, AddIns (or your base project) can create new element types for other data by adding custom doozers. Doozers are the classes that create the objects from the Codons. ICSharpCode.Core contains the doozer classes for the Codon types mentioned in the table. Custom doozers will be covered in another article.

However, using Class will be sufficient in most cases. It allows you to put absolutely any object in the AddIn tree. If all of the classes put into a path implement a specific interface (e.g. IMyInterface), you can use:

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

This makes it possible to use the AddInTree to define things like file type handlers or a set of commands being run on some action.

In case an AddIn needs to run an action on application startup, "/Workspace/Autostart" is a predefined path being run when the core is being initialized (immediately after loading the AddIn tree), the objects stored in it must implement ICommand.

The example application

Now let�s get back to our sample application, the little text editor. The main form is called "Workbench" and shows the main menu, a toolbar and the ViewContent. A ViewContent can be anything that can behave remotely like a document. Our example application can only display one ViewContent at a time.

The "Edit" and "Format" menus are intentionally missing: in the next article, we'll add them as AddIn.

The application is just meant to demonstrate what you can do with ICSharpCode.Core; it is not usable as a full-blown text editor because it doesn't support encodings (only UTF-8). As an example AddIn, the download includes a "RichTextEditor" AddIn that enables simple rich text editing.

We will look at building this application in the following steps:

  1. The Startup code required to set up the Core.
  2. The code required to set up the application window.
  3. Loading and saving application settings using the PropertyService.
  4. Implementing menu commands.
  5. Opening files using extendable "display bindings".
  6. Using localizable resources.

To give you a better overview of the project, here is a screenshot of the project explorer:

Startup

Let us take a look at the Startup code (file Start.cs in the project "Startup", method Start.Main) and the features of ICSharpCode.Core used in it:

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

The Workbench class in the "Base" project is the main window of our application. In its constructor (called by Workbench.InitializeWorkbench), it uses the MenuService and ToolbarService to create the content of the main window.

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

The FormLocationHelper is not provided by the Core, but by a helper class in the "Base" project. It makes use of the PropertyService to load and store the location of the main window.

The PropertyService

The Core contains a class called "PropertyService" that can be used to store application settings. Take a look at the code used to save and restore the location of a Form to get an idea of how easy it is to use:

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

The Get and Set methods of the PropertyService are generic methods:

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

The C# compiler infers the type from GetDefaultBounds, which just returns the bounds of the Form centered on the active screen, and reads the property. The Validate method ensures that the position is valid; we do not want to show the Form on a no-longer-existing secondary monitor. When the Form is closed, the new location is saved. PropertyService supports the types providing a TypeConverter, so you can use it with most of the .NET's built-in types and adding support for custom types is also easy. Additionally, the PropertyService supports storing one-dimensional arrays if the array element type has a TypeConverter.

Menu commands

You have already seen that menu commands are declared in the .addin file. Here are the commands specific to our text editor application:

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

Now take a look at the NewFileCommand class (which obviously wasn�t provided by the core itself):

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

The "Owner" workbench is passed automatically while creating the menu or toolbar:

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

The first parameter is the owner of the toolstrip, all commands created for the toolbar will have their Owner property set to the owner passed while creating the tool strip. This is useful while creating context menus for items.

Opening files

Now move on to opening the existing files. We do not know what kind of file the user will try to open, and we want to give AddIn authors the possibility to add support for more file types. Therefore, the file filter used in the OpenFileDialog must be extensible, and the AddIns should be able to create custom view contents for the file chosen by the user:

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

First look at how the file filter is constructed:

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

And the GetFileFilter method:

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

As you can see, the BuildItems method returns an ArrayList of strings in this case. For the file filter, we do not need any "owner", that�s why the second argument to BuildItems is null.

The FileFilter doozer returns strings in the form "name|extensions"; this is used to concatenate the complete filter string.

Now we'll take a look at the creation of the view content. As already said in the "Features" section, the Core does not provide you with predefined classes for this task, but the DisplayBindingManager is easy to write.

We will define a new interface IDisplayBinding and a new path in the AddIn tree. AddIns will be able to add instances of classes implementing the interface to the AddIn tree using the <Class> element. Our DisplayBindingManager constructs those objects and asks each of them to create a view content for the file. The first object that is able to open the file will be used.

Implementing this behavior is easy:

/// <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;
}   }

As you can see, we are simply constructing all the DisplayBinding classes from the AddIn tree. The first display binding that is able to open the file will be used.

The AddIn definition from the "Base" project, Base.addin, tries to open everything as text file. Our "RichTextEditor" example AddIn has to use "insertbefore" to make sure it is used first:

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

If we don't use "insertbefore", the base text editor would display the rich text source code, and our rich text editor would never be asked to open the file.

Here is the code for the display binding class:

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

This approach will cause all the display binding AddIns to be loaded when a file is opened for the first time; in a later article, I will show you a better approach by moving the file extension check into the XML.

Including items from other AddIn tree paths

<Include> is a very useful element for the .addin XML declarations:

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

This will take all the elements from /Workspace/Tools and insert them at the position of the "Include" node. /Workspace/Tools is a kind of "standardized" path for the AddIns that are not tightly coupled with the base application, but just open a new window when they are called in the "Tools" menu. The AddInManager AddIn included in the download uses this path, so you can start the AddInManager and use it to disable and enable RTF integration.

Resources

The core supports localization using resource files for the different languages. The ResourceService reads the resources from multiple locations:

  • The main English StringResources file is usually embedded into the Startup application. It is registered using:
ResourceService.RegisterNeutralStrings(
    new ResourceManager("Startup.StringResources", assembly));
  • The ResourceService automatically reads language-specific .resources files from the directory data/resources (relative to the application root). This is the common way for providing localized strings for the application.

However, it is also possible that AddIns supply their own localized string resources. The AddInManager for example (included in the download) comes with English and German resource files. Both are set to "EmbeddedResource", so the English resources are included in the AddIn assembly and the German resources are put into a satellite assembly.

When the AddInManager is started, it calls:

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

The ResourceService will load the string resources from the AddIn assembly and it will look for a satellite assembly for the current language. ResourceService.GetString() will probe all the registered resources and return the string for the current language.

However, we cannot simply call a method in the XML files, so we have to use something different there.

The StringParser

The StringParser is a static class in the core that expands "{xyz}"-style property values. The StringParser is used for all labels of menu items in the AddIn tree, so you can use it to include translated strings or other variables.

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

You can use ${res:ResourceName} to include strings from the ResourceService. You can also use ${property:PropertyName} to include values from the PropertyService, or ${env:VariableName} to include environment variables. Additional variables can be set using StringParser.Properties. Moreover, you can register new prefixes by using PropertyService.PropertyObject. A property object can be any object � the members are accessed using Reflection. ${exe:PropertyName} can be used to access any property of the FileVersionInfo object of the entry assembly, e.g. ${exe:FileVersion}.

License

ICSharpCode.Core and AddInManager are licensed under the terms of the GNU Lesser General Public License. In short, you can use the libraries in commercial projects, but you have to publish the source code for any modifications done to the libraries.

The example core from this article "Base" and "Startup" can be used freely, it is BSD-licensed.

Summary

This article shows you how you can use ICSharpCode.Core for your own applications. We discussed the services provided by the core and showed what you need to implement for your applications. We used the AddInTree to store menu items, toolbar items, file filter entries and our own custom objects. Lazy-loading, custom doozers and conditions will be explained in the next article.

History

  • 3rd January, 2005: Article published (based on SharpDevelop 2.0.0.962).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here