Skip to content
This repository has been archived by the owner on Sep 4, 2024. It is now read-only.

Custom Extension Node Types

Kirill Osenkov edited this page Apr 28, 2017 · 3 revisions
Home > Reference Manual > Custom Extension Node Types

Every extension point has to declare the type of extension node that it accepts. Add-ins can register new extension nodes to that extension point. At run-time, extension nodes are represented by instances of the class Mono.Addins.ExtensionNode or instances of a subclass of it. Hosts can query extension points and get instances of ExtensionNode to extract whatever extension information is needed from them.

Mono.Addins provides the class TypeExtensionNode, a type of extension node which covers a very common extension scenario: the host defines an interface (or type), and the add-in has to implement it. At run-time TypeExtensionNode represents a type registered by an add-in, and a host can create instances out of it.

TypeExtensionNode may be enough for simple applications, but more complex applications will need to define new extension node types, so more complex extension information can be declared in extension points. Also, extension nodes don't always need to represent type implementations. In those cases, a host will need to implement custom extension node types.

Custom extension node implementation

Creating extension node types is very simple. Let's see one example:

namespace TextEditor
{
	public class FileTemplateNode: ExtensionNode
	{
		[NodeAttribute]
		string resource;
		
		[NodeAttribute]
		string name;
		
		public string Name {
			get { return name != null ? name : Id; }
		}
		
		public virtual string GetContent ()
		{
			using (StreamReader sr = new StreamReader(Addin.GetResource (resource))) {
				return sr.ReadToEnd (); 
			}
		}
	}
}

This extension node type is used in the Text Editor example to register file templates. When creating a new file, the user can choose a file template, so the new file will include the content provided by the template.

This class does several interesing things:

  • It inherits from Mono.Addins.ExtensionNode. All extension node types must be a subclass of this type.
  • It declares a field named resource and marks it with the NodeAttribute custom attribute. When creating a node instance, all fields marked with this attribute will be initialized using attribute values taken from the node.
  • It implements a GetContent method which gets the content of a template using Addin.GetResource. The Addin property is declared in ExtensionNode and provides access to add-in resources and types.

This node type would be able to represent nodes registered in an extension like this:

<Addin ...>
	...
	<Extension path="/TextEditor/Templates">
		<FileTemplate name="README" resource="readme-template.txt" />
		<FileTemplate name="ChangeLog" resource="changelog-template.txt" />
	</Extension>
	...
</Addin>

When calling AddinManager.GetExtensionNodes(), the add-in engine will read all registered elements and will create the corresponding ExtensionNode subclass instance for each of them. It will also transfer attribute values to fields marked with [NodeAttribute].

Extension nodes are not just data containers, but they can also implement some logic. In this example the host application doesn't need to know where does a FileTemplateNode get the template content from. This logic is hidden in FileTemplateNode. FileTemplateNode happens to get the content from a resource, but it might allow getting it from other places in the future.

The [NodeAttribute] attribute can be applied to fields and properties. It has several properties which allow defining and documenting the behavior of the attribute:

Property Description
Name Name of the attribute, to be used when it is different from the name of the field or property
Required When set to 'true' specifies that it is mandatory to specify a value for the attribute
Description Description of the purpose of the attribute
Localizable When set to 'true', the value of the field or property is expected to be a string id which will be localized by the add-in engine
ContentType Allows specifying the type of the content of a string attribute. This is for documentation purposes only.

Extension node deserialization

The [NodeAttribute] custom attribute can be used to specify fields and properties that have to be initialized from node attributes. By default, a field is will get its value from an attribute with the same name, although it is possible to specify a different name by setting the NodeAttribute.Name property. NodeAttribute also has a Required property for specifying if an attribute is mandatory or not. So for example the ''resource'' filed might be declared like this:

public class FileTemplateNode: ExtensionNode
{
	...
	// Will match elements like <FileTemplate resource-name="blah" />
	// The true parameter specifies that the attribute is required
	[NodeAttribute ("resource-name", true)]
	string resource;
	...
}

ExtensionNode subclasses can also override the virtual method Read (NodeElement elem) to take control on the process of loading a ExtensionNode instance out of an extension node. If the Read method is overridden (and if it doesn't call the base class) [NodeAttribute] attributes will be ignored.

[NodeAttribute ("resource-name", true)]
public class FileTemplateNode: ExtensionNode
{
	...
	string resource;

	protected override void Read (NodeElement elem)
	{
		// This is equivalent to the previous example, but more
		// eficient since reflection is not involved here
		resource = elem.GetAttribute ("resource-name");
	}
	...
}

Notice the [NodeAttribute] attribute declaration applied to the class. When using custom deserialization, [NodeAttribute] can be applied to the class (or to a field) to declare the extension node attributes. This information however is only used for documentation purposes, and the add-in engine will not use it to do any kind of check. All checks must be done by the Read method override.

Getting add-in types, files and resources

As seen in the previous examples, the object returned by the ExtensionNode.Addin property can be used to get information from the add-in that created a node. It is a protected property, and it is intended to be used by ExtensionNode subclasses to get whatever content is needed. There are three methods which can be used to get such content:

  • GetResource (string name): returns a resource embedded in any of the assemblies included in the add-in.
  • GetType (string name): returns a type implemented in any of the assemblies included in the add-in, or in any of the add-ins on which this add-in depends.
  • GetFilePath (string name): Returns a path to a file which has been deployed together with the add-in.

Working with add-in files

It is possible to distribute files together with the add-in, and access those files from extension nodes. To distribute a file with an add-in, the file has to be declared in the Runtime section. For example, let's say we improve FileTemplateNode, so it can get the template not only from resources, but also from files distributed together with the add-in. An add-in declaration might look like this:

<Addin namespace="TextEditor" id="Xml">
	...
	<Runtime>
		<Import assembly="TextEditor.Xml.dll" />
		<Import file="someTemplate.xml" />
	</Runtime>
	...
	<Extension path="/TextEditor/Templates">
		...
		<FileTemplate name="SomeTemplate" fileName="someTemplate.xml" />
		...
	</Extension>
	...
</Addin>

The FileTemplateNode class would need to handle the new fileName attribute:

namespace TextEditor
{
	public class FileTemplateNode: ExtensionNode
	{
		[NodeAttribute]
		string resource;
		
		[NodeAttribute]
		string fileName;
		
		[NodeAttribute]
		string name;
		
		public string Name {
			get { return name != null ? name : Id; }
		}
		
		public virtual string GetContent ()
		{
			StreamReader sr;
			if (resource != null)
				sr = new StreamReader (Addin.GetResource (resource));
			else if (fileName != null)
				// GetFilePath returns the full path to the add-in file
				sr = new StreamReader (Addin.GetFilePath (fileName));
			else
				return null;

			using (sr) {
				return sr.ReadToEnd (); 
			}
		}
	}
}

The private data directory

Every add-in has a private directory which can be used to store add-in specific files created at run time. This directory is created the first time that access to it is requested, and will be deleted when the add-in is uninstalled.

For example, let's say we want to support compressed file templates in FileTemplateNode. The GetContent method would need to uncompress the file before reading it, and it should be done only once. The implementation might look like this:

public virtual string GetContent ()
{
	StreamReader sr;
	if (resource != null) {
		sr = new StreamReader (Addin.GetResource (resource));
	}
	else if (fileName != null) {
		
		// Get the full path to the add-in file
		string filePath = Addin.GetFilePath (fileName);
		
		// If it is a gzipped file, it needs additional processing
		if (filePath.EndsWith (".gz")) {
			
			// The file will be unzipped in an add-in specific folder
			string unzippedFile = Path.Combine (Addin.PrivateDataPath, Path.GetFileNameWithoutExtension (filePath));
			
			// Unzip only once
			if (!File.Exists (unzippedFile))
				UnzipFileTo (filePath, unzippedFile);
			
			filePath = unzippedFile;
		}
		sr = new StreamReader (filePath);
	}
	else
		return null;
	using (sr) {
		return sr.ReadToEnd (); 
	}
}

Default node name and description

When an extension point does not provide a name for a node type, a default node name will be used. This default node name is the name of the class that implements it.

It is also possible to provide a custom default name, and also a description, to be used in those cases. This information can be provided using the [ExtensionNode] attribute:

[ExtensionNode ("FileTemplate", "A file template the user can choose when creating a new file")]
class FileTemplateNode: Mono.Addins.ExtensionNode
{
	...
}

Handling children

The property ExtensionNode.ChildNodes returns a list of children of a node. ExtensionNode implementations can query this list in order to build objects which are composed by several nodes.

For example, the TextEditor.SubmenuNode extension node, which represents an extensible submenu of the main menu, might be implemented like this:

public class SubmenuNode: MenuNode
{
	[NodeAttribute]
	string label;
	
	public override Gtk.MenuItem GetMenuItem ()
	{
		// Create the menu item
		Gtk.MenuItem it = new Gtk.MenuItem (label);
		Gtk.Menu submenu = new Gtk.Menu ();

		// Iterate through all children, and add a menu item for each of them
		foreach (MenuNode node in ChildNodes)
			submenu.Insert (node.GetMenuItem (), -1);
		it.Submenu = submenu;
		return it;
	}
}

// Abstract class to be subclassed by any kind of node that
// can generate a menu item.
public abstract class MenuNode: ExtensionNode
{
	public abstract Gtk.MenuItem GetMenuItem ();
}

The ChildNodes list may dynamically change while the application is running as a result of add-ins being enabled/disabled, or if the status of some conditions change. There are some overridable methods which are called when this happen: OnChildNodeAdded, OnChildNodeRemoved and a more generic OnChildrenChanged.

The [ExtensionNodeChild] attribute allows declaring the type of the child nodes of a node:

[ExtensionNode ("Menu")]
[ExtensionNodeChild (typeof(MenuItemNode))]
[ExtensionNodeChild (typeof(MenuSeparatorNode))]
[ExtensionNodeChild (typeof(SubmenuNode))]
public class SubmenuNode: MenuNode
{
	...
} 

[ExtensionNode ("MenuSeparator")]
public class MenuSeparatorNode: MenuNode
{
	...
}

[ExtensionNode ("MenuItem")]
public class MenuItemNode: MenuNode
{
	...
}

Notice that it is allowed to use recursive node type references.

Localizable extension nodes

Some extension node types may have attributes that need to be localizable. For example, the label of a menu item, or (in the text editor example) the name of a file template. Any text attribute can be made localizable by setting the Localizable property of NodeAttribute to ''true''. For example:

namespace TextEditor
{
	public class FileTemplateNode: ExtensionNode
	{
		...
		
		[NodeAttribute (Localizable=true)]
		string name;
		
		public string Name {
			get {
				// 'name' will be already localized here
				return name != null ? name : Id; 
			}
		}

		...
	}
}

When an attribute is declared localizable like this, the add-in engine will use the add-in localizer bound to the extension node to get a translation of the string, and will assign the translated string to the field.

When using custom deserialization, the RuntimeAddin.Localizer property can be used to get the localizer bound to the add-in declaring the extension node. The following example would be equivalent to the previous one:

namespace TextEditor
{
	[NodeAttribute ("name", Localizable=true)]
	public class FileTemplateNode: ExtensionNode
	{
		...
		
		string name;
		
		protected override void Read (NodeElement elem)
		{
			name = Addin.Localizer.GetString (elem.GetAttribute ("name"));
		}

		...
	}
}

The [NodeAttribute] attribute declaration is used in this case only for documentation purposes.

See Localization of Add-ins for more information about creating localizable add-ins.

Controlling add-in loading

Mono.Addins supports lazy loading of add-ins. Add-ins are never explicitly loaded, but they are loaded only when resources or types from that add-in are required. It is important to take this behavior into account when designing extension nodes. Let's see for example the implementation of the an extension node for registering toolbar buttons:

namespace TextEditor
{
	public class ToolButtonNode: ExtensionNode
	{
		// Name of the icon
		[NodeAttribute]
		string icon;
		
		// Name of the class that implements the command
		// to run when clicking the button
		[NodeAttribute]
		string commandType;
		
		public Gtk.ToolItem GetToolItem ()
		{
			// Create the button and subscribe the clicked event
			Gtk.ToolButton but = new Gtk.ToolButton (icon);
			but.Clicked += OnClicked;
			return but;
		}
		
		void OnClicked (object s, EventArgs a)
		{
			// Run the command when clicking the button
			ICommand command = (ICommand) Addin.CreateInstance (commandType);
			command.Run ();
		}
	}
}

The commandType attribute will have the name of a type which is supposed to implement ICommand. When the button is clicked, the event handler creates an instance of the command and executes it. The call to Addin.CreateInstance () will force the loading of the add-in that registered that node, since CreateInstance needs to load the type of the command. However, this will only happen when the button is clicked. That is, it would be possible to create a complete toolbar out of add-in buttons without having to load the add-ins that define them.

An extension node may need to know when the add-in that defined it has been loaded or unloaded, since it may need to do some initialization or cleanup work. ExtensionNode has two virtual methods OnAddinLoaded and OnAddinUnloaded which can be used for this purpose.

Clone this wiki locally