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)) {
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:
- The Startup code required to set up the Core.
- The code required to set up the application window.
- Loading and saving application settings using the
PropertyService
.
- Implementing menu commands.
- Opening files using extendable "display bindings".
- 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:
LoggingService.Info("Application start");
Assembly exe = typeof(Start).Assembly;
FileUtility.ApplicationRootPath = Path.GetDirectoryName(exe.Location);
LoggingService.Info("Starting core services...");
CoreStartup coreStartup = new CoreStartup("Test application");
coreStartup.PropertiesName = "AppProperties";
coreStartup.StartCoreServices();
ResourceService.RegisterNeutralStrings(
new ResourceManager("Startup.StringResources", exe));
ResourceService.RegisterNeutralImages(
new ResourceManager("Startup.ImageResources", exe));
LoggingService.Info("Looking for AddIns...");
coreStartup.AddAddInsFromDirectory(
Path.Combine(FileUtility.ApplicationRootPath, "AddIns"));
coreStartup.ConfigureExternalAddIns(
Path.Combine(PropertyService.ConfigDirectory, "AddIns.xml"));
coreStartup.ConfigureUserAddIns(
Path.Combine(PropertyService.ConfigDirectory, "AddInInstallTemp"),
Path.Combine(PropertyService.ConfigDirectory, "AddIns"));
LoggingService.Info("Loading AddInTree...");
coreStartup.RunInitialization();
LoggingService.Info("Initializing Workbench...");
Workbench.InitializeWorkbench();
try {
LoggingService.Info("Running application...");
Application.Run(Workbench.Instance);
} finally {
try {
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.
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);
ShowContent(new TextViewContent());
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:
public interface IDisplayBinding
{
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).