Extensions and extension points
Trados Studio offers a predefined set of extension points. Your plug-ins can add custom functionality to Trados Studio by defining extensions that target these extension points.
Extensions
An extension is an individual unit of logic that provides functionality for Trados Studio through a specific extension point. You can create an extension by following the steps below:
- Create a class in your plug-in assembly.
- Decorate the class with the attribute that identifies the extension point it is targeting.
- Implement the interface that is required by the extension point.
Extension points
An extension point is a point in Trados Studio that allows adding extensions to it. You can define an extension point by following the steps below:
- Create a class in your plug-in assembly and derive it from the ExtensionAttribute base.
- Decorate the class with the ExtensionPointInfoAttribute attribute.
- Make sure that the class has a default parameterless constructor. This is needed because the plugin manifest generator uses XML serialization to save attribute information.
Other plug-in developers can create extensions for the extension points you define, by following the procedure described under Extensions.
The ExtensionAttribute class defines the properties that plug-in developers can provide with their extensions:
- Id: A unique id for the extension
- Name: A friendly name for the extension.
- Description: A description of the extension.
- Icon: An optional icon representing the extension.
The ExtensionPointInfoAttribute class specifies the name and type of the extension point:
- The name can be used by a plug-in manager UI to represent the extension point.
- The type can be either static or dynamic, referring to whether this extension point allows enabling or disabling of one or more of its extensions without having to restart the application.
Example: Defining an extension point
We will demonstrate the various topics presented in this article using a message transmitter example. Our requirement is that the host application is able to use pluggable message transmitters that are defined in plug-ins.
First, we need to define an extension point:
[ExtensionPointInfo("Message Transmitters", ExtensionPointBehavior.Static)]
public class MessageTransmitterAttribute : ExtensionAttribute
{
/// <summary>
/// Constructor for XML serialization
/// </summary>
public MessageTransmitterAttribute()
{
}
/// <summary>
/// Constructor using basic properties.
/// </summary>
public MessageTransmitterAttribute(string id, string name, string description)
: base(id, name, description)
{
}
/// <summary>
/// Gets or sets the cost in dollar per character in the message.
/// </summary>
public double CostPerCharacter { get; set; }
/// <summary>
/// Validates that the extension implements the IMessageTransmitter interface.
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public override void Validate(
Sdl.Core.PluginFramework.Validation.IExtensionAttributeInfo info,
Sdl.Core.PluginFramework.Validation.IExtensionValidationContext context)
{
base.Validate(info, context);
context.ValidateRequiredInterface(typeof(IMessageTransmitter));
}
}
The purpose of the message transmitter attribute is to allow plug-in developers to use it to annotate their message transmitter extension classes, to make them known as message transmitter implementations to the extension point.
Note that we have defined an extra property, CostPerCharacter, which indicates the cost in dollars for each character sent in a message.
Since all these properties will be extracted to the plug-in manifest file by the plug-in manifest generator, the host application will be able to access their values without having to create an instance of the actual transmitters or even load the plug-in assembly. Since the plug-in manifest generator uses XML serialization to save the attribute information, the attribute has have a default, parameterless constructor.
We still have to define which functionality is required from an extension class to be accepted as a valid message transmitter by the host application. This is done by defining an interface:
public interface IMessageTransmitter
{
void SendMessage(string message);
}
This simple interface contains one method, SendMessage, to be called by the host application to send the message.
Example: Creating extensions for an extension point
Let's define a message transmitter which transmits messages by email, the EmailMessageTransmitter
class:
[MessageTransmitter(
Id = "email",
Name = "E-mail Transmitter",
Description = "Send messages via e-mail",
CostPerCharacter = 0.1)]
public class EmailMessageTransmitter : IMessageTransmitter
{
public void SendMessage(string message)
{
Console.WriteLine();
Console.WriteLine(String.Format("Email: {0}", message));
Console.WriteLine();
}
}
The EmailMessageTransmitter
class implements the IMessageTransmitter
interface. On top of that, the class is annotated with the extension attribute, MessageTransmitter
, which we defined earlier, providing an id, a name, a description and the cost per character when sending messages using this transmitter.
Similar to the email transmitter, we also define an SMS message transmitter in exactly the same way. We can do this within the same plug-in project, because a plug-in project can contain multiple extensions.
[MessageTransmitter(
Id = "sms",
Name = "SMS Transmitter",
Description = "Send messages via SMS",
CostPerCharacter = 0.5)]
public class SMSMessageTransmitter : IMessageTransmitter
{
#region IMessageTransmitter Members
public int CostPerMessage
{
get { return 10; }
}
public void SendMessage(string message)
{
Console.WriteLine();
Console.WriteLine(String.Format("SMS: {0}", message));
Console.WriteLine();
}
#endregion
}
Auxiliary Extension Attributes
In some cases, you might need to add some extra metadata to a certain extension implementation, on top of what is defined in the extension attribute and it might be impractical to add these properties to the extension attribute itself.
An example of this is for instance a plug-in user action. The extension attribute for the action can define name, icon, tooltip etc, but you also need to specify on which menus, toolbars and context menus this user action will be available. For cases like this, the plug-in framework provides auxiliary extension attributes.
You can only apply one extension attribute to an extension implementation: this is the attribute that uniquely identifies the extension point the extension implementation is targeting. On top of this, you can decorate the implementation class with as many auxiliary extension attributes as you like.
An auxiliary extension attribute needs to derive from the AuxiliaryExtensionAttribute base class. For instance, we can define a ToolBarLocation
auxiliary attribute, which has a ToolBarId
property that can be used to specify on which tool bar the action should appear. For menus, we can define a similar MenuLocation
attribute:
public class ToolBarLocationAttribute : AuxiliaryExtensionAttribute
{
public string ToolBarId { get; set; }
}
public class MenuLocationAttribute : AuxiliaryExtensionAttribute
{
public string MenuId { get; set; }
}
Now the plug-in action definition can be written like this:
[Sdl.Desktop.Platform.CommandBars.Action(
Id = "mypluginbutton",
Name = "MyPluginAction_Name",
Description = "MyPluginAction_ToolTipText")]
[ToolBarLocation(ToolBarId = "StandardToolBar")]
[MenuLocation(MenuId = "FileMenu")]
public class MyPluginButton2 : IPluginButton
{
// ...
}
The collection of all auxiliary attributes for an extension can be retrieved using the AuxiliaryExtensionAttributes
property.
Sortable Extension Points
The order in which extensions are processed for a particular extension point is essentially random. However, it is a fairly common requirement for extensions to have a certain order and for extensions themselves to be able to specify where they want to "appear" relative to the other extensions for that extension point. An example of this are menu item extensions: when creating a new menu item extension, there is clearly the need to specify where that menu item should appear relative to other menu items.
In order to solve this common use case, the plug-in framework provides the SortableExtensionAttribute extension attribute class. This attribute extends the standard ExtensionAttribute by adding two additional properties: InsertBefore
and InsertAfter
. Extensions for an extension point that derives from SortableExtensionAttribute can use these properties to specify the Id of other extensions they want to appear before or after. In addition to single Ids, these properties also accept multiple comma-separated Ids.
You can use the SortedObjectRegistry<TSortableExtensionAttribute, TExtensionType> class to create instances of all extensions for a sortable extension point and sort these according to the values of the InsertBefore
and InsertAfter
properties.
In case you want more control when ordering extensions, you can use the TopologicalSort< T> class, which implements the sorting algorithm. You then need to provide wrapper objects that implement ITopologicalSortable for each extension.
Compile-time extension validation
In order to catch as many developer errors as possible at compile-time, the plug-in framework provides a mechanism for extension point developers to validate extension definitions at compile-time.
Every ExtensionAttribute and AuxiliaryExtensionAttribute has validation methods which are called during the build process. These methods then have the ability to report errors and warnings, which will be displayed in Visual Studio as standard compiler errors.
Extension Attribute Validation
The ExtensionAttribute type has a Validate
method, which by default validates that the user has specified values for the Id
and the Name
property. When developing and extension point, this method can be overridden to perform additional validation.
Auxiliary Extension Attribute Validation
The AuxiliaryExtensionAttribute type also has a Validate
method, which by default doesn't do any special validation. Extension point developers can override this method to perform additional validation for an auxiliary extension attribute.
Example: Compile-time extension validation
In our message transmitter example, the extension point checks whether the extension implements the IMessageTransmitter interface. If an extension does not implement this interface, an error will be generated at compile time.
/// <summary>
/// Validates that the extension implements the IMessageTransmitter interface.
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public override void Validate(
Sdl.Core.PluginFramework.Validation.IExtensionAttributeInfo info,
Sdl.Core.PluginFramework.Validation.IExtensionValidationContext context)
{
base.Validate(info, context);
context.ValidateRequiredInterface(typeof(IMessageTransmitter));
}
Consuming extensions using the PluginRegistry and ObjectRegistry
The IPluginRegistry object is the main object that can be used by the host application or component in order to access and instantiate the available plug-ins.
The PluginManager static class, which is the entry point to the plug-in framework object model, provides various ways to create instances of the IPluginRegistry object. By default, plug-ins are installed in the applications installation directory and plug-in manifest and resource files are installed in a "plugins" subdirectory. In order to access these plugins, you can use the DefaultPluginRegistry
property, which returns an IPluginRegistry instance, that has been configured to load plug-ins from that default location.
You can easily instantiate a list of all the extension implementations registered with a particular extension point using the ObjectRegistry<TExtensionAttribute, TExtensionType> class. This is a generic class accepting two template parameters:
TExtensionAttribute
: The extension attribute type defining the extension point.TExtensionType
: The common interface that all extensions for this extension point must implement. TheCreateObjects
method simply instantiates all the extension implementations and returns them in a typed array.
For sortable extension points, you can use the corresponding SortedObjectRegistry<TSortableExtensionAttribute, TExtensionType> class.
Example: Using the PluginRegistry
Returning to our message transmitter example, we want to write some custom code that sends a message using a message transmitter. Also, we will let the user select the transmitter they would like to use.
Message transmitters are defined in plugins. In order to use them we need to do several things.
First, we get the extension point from the plug-in registry. An extension point is represented by the IExtensionPoint interface and provides access to all extensions discovered for that extension point. We identify our desired extension point by passing MessageTransmitterAttribute
as the template parameter:
IExtensionPoint extensionPoint = PluginManager.DefaultPluginRegistry.
GetExtensionPoint<MessageTransmitterAttribute>();
Second, prompt the user to enter a message:
Console.Write("Enter your message: ");
string message = Console.ReadLine();
Next, list all the available message transmitters, along with their name and the cost per character. This is done by iterating over the Extensions collection of the extension point, which contains IExtension objects. These provide access to the extension attribute that was used to annotate the respective extension implementation classes. The name and cost per character can be retreived from that MessageTransmitterAttribute
:
int i = 0;
Console.WriteLine("Available message transmitters:");
foreach (IExtension extension in extensionPoint.Extensions)
{
MessageTransmitterAttribute extensionAttribute =
(MessageTransmitterAttribute)extension.ExtensionAttribute;
Console.WriteLine(String.Format("{0}) {1} (cost per character: ${2})",
++i,
extensionAttribute.Name,
extensionAttribute.CostPerCharacter));
}
Console.WriteLine(String.Format("Choose a message transmitter (1-{0}):", i));
int number = Convert.ToInt32(Console.ReadLine());
Now get the extension object corresponding to the user's choice, and also get hold of the extension attribute instance:
IExtension selectedExtension = extensionPoint.Extensions[number - 1];
At this point we are ready to create the actual message transmitter implementation, using the IExtension object:
IMessageTransmitter selectedTransmitter =
(IMessageTransmitter)selectedExtension.CreateInstance();
Finally, call the SendMessage method to send the message:
selectedTransmitter.SendMessage(message);
The application produces the following output:
Example: Using the ObjectRegistry
In the example above, we explicitly went through the plug-in registry object model to illustrate how the various classes interact. As an easier alternative, we can quickly create a list of extension implementation objects using the ObjectRegistry<TExtensionAttribute, TExtensionType> :
ObjectRegistry<MessageTransmitterAttribute, IMessageTransmitter> objectRegistry
= new ObjectRegistry<MessageTransmitterAttribute, IMessageTransmitter>
(PluginManager.DefaultPluginRegistry);
IMessageTransmitter[] messageTransmitters = objectRegistry.CreateObjects();