Writing an Add-In

From $1
    Table of contents

    Version as of 23:53, 19 Sep 2014

    to this version.

    Return to Version archive.

    View current version

    Introduction

    warning: this tutorial is hopelessly out of date and will not work with up-to-date monodevelop versions

    MonoDevelop has a powerful extension system which can be used by developers to integrate additional functionality to the IDE (or more generally speaking, to the MonoDevelop platform).

    MonoDevelop's extension system is based on a tree of extension points. An extension point is just a placeholder where add-ins can add new items which provide extra functionality. Extension points are identified with a path, which specifies its location in the tree. For example, the path: '/SharpDevelop/Workbench/MainMenu' identifies the extension point which can be used by add-ins to add items to the main menu.

    Add-ins not only have to add new elements to extension points, but usually they'll have to implement some classes or interfaces. For example, an add-in which adds new menu items to the main menu may need to implement classes for handling those new commands.

    The document Extension Tree Reference has a complete description of extension tree and the requirements of each extension point.

    Add-ins can also add new extension points to the extension tree. For example, the SourceEditor add-in (the add-in which implements the main MonoDevelop source code editor) adds a new extension point to the tree: '/AddIns/DefaultTextEditor/EditActions'. Other add-ins can then extend the editor by implementing and registering custom edit actions.

    MonoDevelop can be seen as a hierarchy of add-ins. Each add-in can add new items to extension points, and can add new extension points for other add-ins to extend.

    The document 'The SharpDevelop add-in tree architecture' by Mike Krueger and Bernd Spudia has some information about the SharpDevelop add-in architecure, from which MonoDevelop was derived and further developed. This article contains several paragraphs directly taken from that document.

    Contents of an add-in

    A MonoDevelop add-in is composed by the following files:

    • An add-in description file which declares the extensions implemented by the add-in. This file is mandatory.
    • One or more assemblies, which implement the extensions.
    • Other support files needed by the add-in (such as bitmaps, html files or whatever is needed at run-time).

    Most of add-ins will only have the add-in description file and one assembly implementing the extensions. Some add-ins will need additional assemblies. For example, the Stetic add-in contains a copy of the Stetic libraries.

    Some add-ins will need support files. For example, the html page and css styles file used by the Welcome Page add-in are defined in the add-in as support files.

    All files of an add-in can be packaged together in an .mpack file, which is just a zip file containing all files. There is a chapter in this article which explains how to generate mpack files.

    The add-in description file

    This file describes the content of an add-in. It is an XML file with four sections:

    • The header: provides basic information such as the id, name, author and description.
    • Runtime element: Specifies assemblies and files to be loaded at run-time.
    • Dependencies element: Specifies other add-ins and assemblies required to run the add-in.
    • Extensions: A list of all extensions implemented in the add-in.

    Here is a more detailed description of each section:

    Header

    The header is the AddIn root element of the xml file. The most important attributes are the following (all of them mandatory):

    • id: identifier of the add-in. Must be an unique identifier among all add-ins. Using a namespace prefix is recomended, such as in 'MonoDevelop.SourceEditor'.
    • name: display name of the add-in.
    • version: version number of the add-in.

    Other optional attributes are:

    • compatVersion: oldest compatible version of the add-in (where compatible means that it doesn't break the ABI). This attribute only makes sense for add-ins that can be extended, so others add-ins can depend on it. For example, an add-in with version 1.6 and compatVersion 1.2 can be used to satisfy dependencies on 1.2 or upper versions.
    • author: name of the author.
    • copyright: copyright and license of the add-in.
    • url: url with more information about the add-in.
    • description: long description of the add-in.
    • category: category under which to show the add-in in the add-in repository.

    The Runtime element

    The Runtime element specifies the files required to run the add-in. Each file is specified in an Import element. Two kind of files can be imported: assemblies and support files. To import an assembly, just specify the relative path to the assembly in an assembly attribute. To import a support file, use the file attribute.

    Here is an example of an add-in importing an assembly and some support files:

    <Runtime>
    	<Import assembly="WelcomePage.dll"/>
    	<Import file="WelcomePage.css"/>		
    	<Import file="WelcomePage.xsl"/>		
    	<Import file="mono-bg.png"/>		
    	<Import file="mono-logo.png"/>		
    </Runtime>
    

    An add-in can import more than one assembly. Assemblies will be loaded in the order they are declared in the Runtime element, so if one add-in depends on another one, make sure the dependent is declared the latest.

    Dependencies

    If an add-in extends or makes use of classes implemented in another add-in, it must declare this dependency in the Dependencies element. Dependencies on external assemblies must also be declared. For example:

    <Dependencies>
    	<AddIn id="MonoDevelop.Core" version="0.10.0"/>
    	<AddIn id="MonoDevelop.Core.Gui" version="0.10.0"/>
    	<AddIn id="MonoDevelop.Projects" version="0.10.0"/>
    	<AddIn id="MonoDevelop.Projects.Gui" version="0.10.0"/>
    	<AddIn id="MonoDevelop.Ide" version="0.10.0"/>
    	<Assembly name="nunit.framework, Version=2.2.0.0" package="nunit"/>
    </Dependencies>
    

    This example add-in specifies that it depends on several MonoDevelop.* add-ins, and also that it requires the nunit-framework assembly to be available in the GAC.

    Extensions

    Add-ins can use the Extension element to specify new elements to be added to the extension tree. The tree location where to add the elements is specified in the path attribute. For example:

    <Extension path = "/SharpDevelop/Commands">
    	<Command id = "MonoDevelop.WelcomePage.WelcomePageCommands.ShowWelcomePage"
    		defaultHandler = "MonoDevelop.WelcomePage.ShowWelcomePageHandler"
    		_label = "Welcome Page" 
    		icon = "gtk-home"/>
    </Extension>
    

    The format of the tree path is like the format for paths in a Unix directory tree. Inside the Extension element there are the new elements to be added. Those elements are internally referred as Codons.

    Codons have attributes which specify the behaviour or state of some element of the program. Every codon can have an ID which identifies it in the tree; the codon is then inserted into the tree beneath the extension path + ’/’ + id. The id attribute is not mandatory, but only codons with an ID will be able to be referenced or extended by other codons.

    An add-in will usually add elements to several extension points of the extension tree. See the document Extension Tree Reference for a complete description of all available extensions.

    Arranging codons

    One requirement for menu items is that they need to be in some sort of order. When defining a menu, we expect the options to be shown in the order they are defined.

    When there is only a single xml file defining the menu this is not a problem. Problems arise when the definitions are scattered across more than one XML file.

    All codons have two common attributes which can be used to specify the placement in the tree: insertafter and insertbefore.

    The insertafter attribute can be applied to a codon to specify the ID of the codon after which the it has to be inserted. For example:

    <ItemSet id="MyMenu" insertafter="File" _label="My menu">
    

    The insertafter attribute here ensures that the 'MyMenu' menu is inserted after the 'File' menu. Notice that the codon will be inserted just after the referenced codon, unless there is another add-in which defines a codon with the same insertafter reference. In this case, the codons from one add-in will be inserted after the codons of the other add-in (the order depends on which add-in was loaded first).

    If you want to insert several codons after one existing codon, the insertafter attribute only needs to be applied to the first one in the list. Once the insertion point is specified for one codon, all codons following that one will be inserted after it. For example:

    <Extension path = "/SharpDevelop/Workbench/MainMenu">
      <ItemSet id="MyMenu1" _label="My menu 1" insertafter="File"> ... </ItemSet>
      <ItemSet id="MyMenu2" _label="My menu 2"> ... </ItemSet>
      <ItemSet id="MyMenu3" _label="My menu 3"> ... </ItemSet>
    </Extension>
    

    The insertbefore attribute works like the insertafter attribute, but it forces the codon where it is specified to be inserted before the referenced codon.

    Example XML

    Here is a complete example of an add-in description file:

    <AddIn	id = "MonoDevelop.WelcomePage" 
    	name = "MonoDevelop Welcome Page"
    	author      = "Scott Ellington"
    	copyright   = "MIT X11"
    	url         = "http://salmonsalvo.net/"
    	description = "Welcome Page for MonoDevelop"
    	category    = "IDE extensions"	   
    	version     = "0.2.0">
    	
    	<Runtime>
    		<Import assembly="WelcomePage.dll"/>
    		<Import file="WelcomePage.css"/>		
    		<Import file="WelcomePage.xsl"/>		
    		<Import file="mono-bg.png"/>		
    		<Import file="mono-logo.png"/>		
    	</Runtime>
    
    	<Dependencies>
    	    <AddIn id="MonoDevelop.Core" version="0.10.0"/>
    	    <AddIn id="MonoDevelop.Core.Gui" version="0.10.0"/>
    	    <AddIn id="MonoDevelop.Projects" version="0.10.0"/>
    		<AddIn id="MonoDevelop.Ide" version="0.10.0"/>
    	</Dependencies>
    	
    	<Extension path = "/SharpDevelop/Commands">
    		<Command id = "MonoDevelop.WelcomePage.WelcomePageCommands.ShowWelcomePage"
    			defaultHandler = "MonoDevelop.WelcomePage.ShowWelcomePageHandler"
    			_label = "Welcome Page" 
    			icon = "gtk-home"/>
    	</Extension>
    
    	<Extension path="/SharpDevelop/Workbench/MainMenu/View">
    		<CommandItem id="MonoDevelop.WelcomePage.WelcomePageCommands.ShowWelcomePage" 
    			insertafter="ViewItemsSeparator2"
    			insertbefore="MonoDevelop.Ide.Commands.ViewCommands.FullScreen" />
    	</Extension>
    	
    	<Extension path = "/MonoDevelop/IDE/StartupHandlers">
    		<Class class="MonoDevelop.WelcomePage.ShowWelcomePageOnStartUpHandler" />
    	</Extension>
    	
    	<Extension path = "/SharpDevelop/Dialogs/OptionsDialog">
    		<DialogPanel id    = "WelcomPageOptions"
    			insertafter = "ToolsOptions"
    			_label = "Welcome Page">
    			<DialogPanel    id = "General"
    				_label = "General"
    				class = "MonoDevelop.WelcomePage.WelcomePageOptionPanel" />
    		</DialogPanel>
    	</Extension>
    </AddIn>
    

    Creating extensible add-ins

    MonoDevelop add-ins can declare new extension points in the extension tree, to be extended by other add-ins. This section explain how to do it.

    Declare new extension points

    New extension points are declared in the add-in description file using the same syntax used to extend existing extension points, with some additions. For example:

    <Extension path = "/MyAddIn/ContextMenu" extension-nodes="CommandItem, SeparatorItem" description="Context menu for MyAddIn.">
    	<CommandItem id="MonoDevelop.Ide.Commands.EditCommands.Copy"/>
    	<CommandItem id="MonoDevelop.Ide.Commands.EditCommands.Cut"/>
    	<CommandItem id="MonoDevelop.Ide.Commands.EditCommands.Paste"/>
    </Extension>
    

    In this case, this new extension point contains some elements defined in the owner add-in. In other cases this won't be needed and the extension point can be left empty.

    The description attribute is an explanation of what is this extension point for.

    The extension-nodes attribute specifies which elements are allowed in this extension point. MonoDevelop defines several element types or codons. A complete list can be found here. However, you can create your own codon types.

    Creating new codons

    To create a new codon you need to create a class which implements ICodon (from MonoDevelop.Core.AddIns namespace). The easiest way of doing it is by subclassing the class AbstractCodon. Then, you'll need to do the following:

    • Apply the CodonNameAttribute attribute to the class, and specify the name of the codon (to be used in xml files).
    • Add one field for each attribute you want to support. Such fields should be marked with the XmlMemberAttribute attribute. You can specify the name of the attribute (if it's different from the name of the field), or set whether the attribute is required or not (using the IsRequired property).
    • Implement the BuildItem method. This method is called after loading the codon and should be used for initializing internal data structures if needed. This method must return an object to be inserted in the tree in place of the codon. In most of cases returning this should be enough.

    Here is a simple example of codon:

    [Description ("A file template.")]
    [CodonNameAttribute("FileTemplate")]
    internal class FileTemplateCodon : AbstractCodon
    {
    	[Description ("Resource name where the template is stored.")]
    	[XmlMemberAttribute("resource", IsRequired = true)]
    	string resource;
    	
    	public string Resource {
    		get { return resource; }
    		set { resource = value; }
    	}
    	
    	public override object BuildItem (object owner, ArrayList subItems, ConditionCollection conditions)
    	{
    		return this;
    	}
    }
    

    Getting elements declared in an extension point

    To get all elements added to an extension point you can use the GetTreeItems() method from the add-in service. for example:

    object[] items = Runtime.AddInService.GetTreeItems ("/MyAddIn/ContextMenu");
    
    

    Notice that the returned array will contain one object per codon found. This object will be the object returned by the BuildItem method of each codon. If you want to get the codons, and not the created items, you can use GetTreeCodons() instead.

    Packaging and publishing add-ins

    MonoDevelop provides tools for packaging and publishing add-ins. This is what you have to do:

    • Make sure that the <Runtime> section of your add-in description file contains references to all files used by the add-in. Don't forget support files needed at run-time.
    • Create a local directory, for example: myaddins
    • Execute "mdtool setup pack path/to/addin.xml -d:myaddins". This will create a .mpack file in myaddins, which is just a zip with all files.
    • Do the above for all your add-ins.
    • Execute "mdtool setup rep-build myaddins". This will create a couple of .mrep files, which are just an index of the add-ins in the repository.
    • Upload all .mpack and .mrep files to a web server

    The URL of the main.mref file is the URL of the add-in repository you just created.


    Lluis 12:55, 7 Apr 2006 (EDT)