Writing plugins for input, processing and output

IPO (input-processing-output) plugins are the heart of NIPO. NIPO defines this plugin structure to enforce reuse of code. Nearly every application will have an input and output of some type. NIPO just abstracts and generalizes this concept. So, if you develop plugins, spend some thoughts on the reuse of your code (are there other applications that might need a similar functionality).

Every plugin in NIPO inherits from NIPO.Plugins.Plugin. This base class defines the common properties of all plugins. It also provides some services to its subclasses (like e.g. logging).

Plugin communication

It is important to understand how plugins exchange data (after all, we want data to flow from input to output). NIPO plugins communicate soley using events. For the normal data flow (input to processing to output) those events are already defined (the name is NewData, defined for input and processing). In order to receive data from another plugin, your plugin must be registered as event handler for this data event. For the normal data flow, this is done automatically by the controller.
Thus, when you select a processing plugin for your NIPO instance, it will automatically be registered for the NewData events of all input plugins (the ProcessData method of the processing plugin is the default event handler). The OutputData method of every output plugin will be the event handler for the ProcessingPlugin.NewData event.
To pass new data to the next plugin, the InputPlugin and ProcessingPlugin base classes provide the RaiseNewData method.
If there are other events defined by plugins, NIPO provides a mechanism to dynamically discover them and lets other plugins register as event handler (this is an advanced topic, please see AdvancedNIPO for details.

The DataTransferObject

To pass data between plugins, the NIPOEventArgs carries a subclass of NIPO.Data.DataTransferObject. This mechanism is used to enable generic data transfer (i.e. without any restrictions on what data to transfer. In NIPO you can process image data as well as strings or any other data). There are altready predefined subclasses of DataTransferObject to enable a quick start. Suitable for most purposes will be the NIPO.Data.GenericDTO. This is a simple generic type where the type parameter defines the data carried by the DTO. Thus, GenericDTO<string> carries strings as payload.
Being generic has one drawback here. We need to cast the incoming data to the type that we expect. But this enables one plugin to possibly process data of various types. Thus, the typical way of processing data is to first check the type and then get the data.
public override void ProcessData(object sender, NIPOEventArgs e)
        {
            if (!(e.Data is GenericDTO<byte[]>)) return;
            if (e.Data.ContentSize <= 0) return;
            
            byte[] data = (e.Data as GenericDTO<byte[]>).Value;
            Array.Reverse(data);
            RaiseNewData(new GenericDTO<byte[]>(data, "byte[]", data.Length, DateTime.Now));
        }
The method above is taken from a processing plugin that receives data from the input. It needy a byte array to work on the data (for this example, it just reverses the array). The plugin checks if the data type suits its needs, then gets and processes the data and calls RaiseNewData to pass data on to the output.
Besides the GenericDTO NIPO also comes with a ListDTO which can be used to pass more than one data object. The user can define own data transfer objects but must make shure that the sending and receiving plugin know this type. If you want to process data from an unsafe context (e.g. data coming from an external native library or process) you could construct a UnmanagedDTO which contains an IntPtr to native data. This enables you to work on this data without having to copy it to the managed context. Of course, there are many other possible DTOs.

Writing an input plugin

Every input plugin is a subclass of NIPO.Plugins.InputPlugin. By definition, every input plugin runs in a dedicated thread. For this, the InputPlugin.Start() method is used. In this method you can perform every input action necessary. InputPlugin also defines a bool member called Abort. Whenever this bool is set to true, your plugin should stop working. The thread will be interrupted and aborted in case the inputplugin does not stop.
An input plugin can call InputPlugin.RaiseNewData(DataTransferObject) to pass data to processing. This method will raise the InputPlugin.NewData event synchronously or asynchronously depending on the plugin configuration.

Writing a processing plugin

Processing plugins are where the work of your application is done. They take the input data (e.g. an image) and process it (e.g. grayscaling, resizing,...) and pass it to the output where it is send into an output channel (e.g. save to file). Every processing plugin must be a subclass of NIPO.Plugins.ProcessingPlugin. A processing plugin receives the data to process in the ProcessingPlugin.ProcessData method. After the data processing is finished, the processing plugin calls RaiseNewData to pass the result on to the output.

Writing an output plugin

The output plugin's work is simple: take the incoming data transfer object and write the data to the specified output channel. This may be a file or a network stream or a database or whatever else. An output plugin is a subclass of NIPO.Plugins.OutputPlugin. The plugin gets the data in the method OutputPlugin.OutputData. Again, a DataTransferObject is used.

How to ensure that plugins from different parties are compatible?

This is a challenging problem. NIPO provides a very generic solution to this. By default the plugins are not checked for compatibility (to speed up development in case you develop all plugins by yourself).
But if you plan to provide your implementation to others, you should consider enabling the compatibility check mechanis. Basically NIPO asks every data producing plugin (plugins which can pass on data, i.e. input and processing) which output types and formats they can produce. After this, it asks the receiving plugins (i.e. processing and output) if they can interpret this data. To enable compatibility checking for your plugin it must implement either IInputChecker and/or IOutputChecker (An input plugin implements only IOutputChecker, an output plugin implements IInputChecker and the processing plugin implements both interfaces). See the interface documentation for details.

Last edited Mar 24, 2010 at 10:25 AM by bedieber, version 7

Comments

No comments yet.