How to write a plugin
Pre-requisites
You will need Visual Studio installed, ideally Visual Studio 2017. If you Google Visual Studio Community Edition you will find links to the free version of Visual Studio.
You will also need to have Virtual Radar Server installed on the build machine.
VRS plugins need to be .NET assemblies. You can use any .NET language to write the plugin, these instructions use C#.
Runtime Environment
We are going to configure Visual Studio so that it compiles the plugin into a sub-directory under an installation of Virtual Radar Server, which makes it easier to test.
However, Windows will not let Visual Studio write into the Program Files folder heirarchy unless you either have UAC turned off or you're running Visual Studio as administrator, neither of which are recommended.
So to get around this we'll start by creating a VRS installation that Visual Studio can write to.
Open up Windows Explorer and create a folder somewhere. Call it VRS-Plugin-Dev. These instructions will
assume that you've created C:\VRS-Plugin-Dev
.
If you already have VRS installed on the build machine then copy everything from C:\Program Files (x86)\VirtualRadar
into the VRS-Plugin-Dev
folder.
If you do not have VRS installed then download and run the installer. Tell it to install into the VRS-Plugin-Dev
folder.
Double-click the VirtualRadar.exe file in VRS-Plugin-Dev
and make sure that it loads.
Shut down VRS before continuing.
Plugin Project
In Visual Studio go to File | New | Project...
.
On the new project screen that pops up select Class Library as the project type and ensure that the
.NET framework dropdown is set to .NET Framework 3.5.
If you cannot see .NET Framework 3.5 in the drop-down list of frameworks then you will need to install support for it.
Come out of Visual Studio and re-run the Visual Studio installer. Go to the individual components tab and tick the option for .NET Framework 3.5.
If you want your plugin to target the version 3 beta of Virtual Radar Server instead of the release version then choose .NET Framework 4.6.1 instead of .NET Framework 3.5.
If you're writing in C# then you should get something like this:
References
Your plugin will need to reference some VRS files. The easiest way to do this is to add the VirtualRadar.PluginLibraries NuGet package to your project.
To do this right-click the plugin project and choose the Manage NuGet Packages...
link. Then click on the Browse
tab and search for VirtualRadar.PluginLibraries. Choose the latest 2.x.x version and install it.
If you want your plugin to target the version 3 beta of Virtual Radar Server then (1) tick the option to show pre-releases in the NuGet search window and (2) install the latest 3.x.x-beta version of VirtualRadar.PluginLibraries.
Build Folder
Right-click the project node and choose Properties
.
Scroll down a bit on the Build screen until you get to the Output section:
Change the Output path to the VRS-Plugin-Dev folder that you created before and
then tack on \Plugins\SamplePlugin
(change SamplePlugin to whatever you've
called your plugin):
Change the Configuration dropdown from Active (Debug) to Release and repeat the changes, setting the output path to the same folder.
Change the Configuration dropdown back to Debug and then close the properties window.
Debug Settings
If you were to try to build and run the project now Visual Studio will complain that class libraries cannot be
started. We need to configure Visual Studio so that when we press Ctrl+F5
it will run the copy of
Virtual Radar Server that we put into the C:\VRS-Plugin-Dev folder, which in turn will load and run
our plugin. Once this has been set up then we should also be able to set breakpoints in our plugin from Visual
Studio and debug it, if required.
Right-click the SamplePlugin project and pick Properties
again.
Select the Debug tab. You should get a window like this:
Select the Start external program option and enter the full path to VirtualRadar.exe in your VRS-Plugin-Dev folder:
Select Release in the Configuration dropdown and repeat for the release build.
Switch the configuration back to Debug and then close the properties window.
DLL File Name
Virtual Radar Server will only look for plugin code in DLL files with names that start with VirtualRadar.Plugin.
Right-click the SamplePlugin project node and pick Properties
again. Go to the Application tab:
Change the Assembly name so that it starts with VirtualRadar.Plugin.
:
Close the properties window.
Manifest File
The manifest file is an XML file that lives in your plugin folder. It has to have the same name as your plugin's DLL but with an XML extension. You use the file to tell VRS whether the plugin will be able to work with the version of VRS that is running.
Right-click the project node and choose Add | New Item...
. In the dialog that pops up
click on Data
and XML File
(do not choose Application Manifest File).
In the Name field enter the name of your project's assembly with an XML extension:
Click Add
. You should now see the XML file in the project.
Right-click the XML file in the solution explorer and choose Properties
. In the properties pane that
appears change the Copy to Output Directory setting to Copy if newer
:
Double-click the XML file to open it if it's not already open. Overwrite the contents with this:
<?xml version="1.0" encoding="utf-8"?> <PluginManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <MinimumVersion>2.4.0</MinimumVersion> <MaximumVersion>2.4.9999</MaximumVersion> </PluginManifest>
This manifest will tell VRS that it should only load the plugin if Virtual Radar Server's version is at least 2.4.0 and at most 2.4.9999. You will need to change the minimum and maximum version numbers if you are targeting a different version of VRS.
If you want your plugin to target the version 3 beta of Virtual Radar Server then you should set the minimum version to 3.0.0 and the maximum version to 3.0.9999.
Save the file and close the editor tab.
Plugin.cs
Plugin.cs is the heart of the plugin, it holds all of the code that Virtual Radar Server will call when it loads your plugin. You can have other classes in your plugin, and your plugin can load any libraries that it needs to, but you must always have a Plugin class so that VRS can talk to you.
When you first created the project Visual Studio will have created an empty class for you and called it Class1.cs.
Right-click the Class1.cs node in the solution explorer and choose Rename
. Change the name to
Plugin.cs
. When Visual Studio asks if you want to rename all references to Class1, click Yes
.
Double-click Plugin.cs. It probably looks something like this:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SamplePlugin { public class Plugin { } }
Add a reference to VirtualRadar.Interface to the module:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using VirtualRadar.Interface;
Change the public class Plugin
line to read public class Plugin : IPlugin
public class Plugin : IPlugin { }
The IPlugin interface is what Virtual Radar Server will look for when it loads your plugin. You can only have one public class in your plugin that implements IPlugin.
Right-click IPlugin and choose Implement Interface | Implement Interface
.
Visual Studio will add a whole bunch of stubs to your class. All of the stubs will just throw the
NotImplemented exception so you'll need to flesh them out.
Properties
Property | Description |
---|---|
HasOptions | Return true if your plugin has an options screen, false if it does not. Out example does not have an options screen so we'll return false. |
Id | Return a short string that identifies your plugin. It must be unique, VRS will not load two plugins that have the same identifier. We'll call our plugin "sample.plugin". |
Name | Return the name that VRS will show in the plugin list for your plugin. We'll return "Sample Plugin" in the example. |
PluginFolder |
You don't touch this property, VRS will fill it in at run-time with the plugin's location on disk. All
you need to do is provide the property so that VRS can write to it. If you're writing the plugin in
C# or VB.NET then you can just provide an automatic property for this:
public string PluginFolder { get; set; } |
Status | This is the status that VRS will show against the plugin in the plugin list. When the status changes we need to tell VRS that it's changed by raising the StatusChanged event. See the code sample for StatusChanged for a full implementation of the property and the event. |
StatusDescription | This is the longer bit of text that VRS shows under the status in the plugin list. You need to tell VRS when the property has changed by raising the StatusChanged event - see the StatusChanged code sample for a full implementation. |
Version | This is the version number that VRS will show against your plugin in the plugin list. Return anything you like. We'll return "1.0" in the example. |
StatusChanged
You communicate the status of your plugin to the user via two properties, Status and StatusDescription. VRS will listen to the StatusChanged event that you implement and update the display with the content of those two properties whenever it is fired.
Typically you would write a function to raise StatusChanged and then have the property setters for Status and StatusDescription call that function whenever the property changes. That way you don't have to remember to raise the event, you just set the property and it all happens automatically.
private string _Status; public string Status { get { return _Status; } set { if(value != _Status) { _Status = value; OnStatusChanged(EventArgs.Empty); } } } private string _StatusDescription; public string StatusDescription { get { return _StatusDescription; } set { if(value != _StatusDescription) { _StatusDescription = value; OnStatusChanged(EventArgs.Empty); } } } public event EventHandler StatusChanged; private void OnStatusChanged(EventArgs args) { var statusChanged = StatusChanged; if(statusChanged != null) { statusChanged(this, args); } }
Functions
VRS will call the functions on the Plugin class at different points in the life-cycle of the plugin. Each function is expected to either perform a particular task or do nothing. I'll list them out in the order in which they are called, although this probably won't be the order in which Visual Studio created them when it created the stubs.
Function | Description |
---|---|
RegisterImplementations |
This is called early on in Virtual Radar Server's load process, once all of the plugins have loaded but before any of the programs main classes have been instantiated. Do not make any calls to Factory.Singleton from here, particularly for interfaces that inherit from ISingleton, you could break things for other plugins. Virtual Radar Server uses a class factory. Instead of instantiating classes directly with the new keyword you will see references to Factory.Singleton.Resolve<ISomeInterface>(); peppered throughout the source. Those references are instantiating new objects via the class factory, the code asks for an implementation of an interface and the class factory creates one and returns it. In this way the classes don't have direct dependencies on each other, which makes life easier when it comes to writing unit tests. It also makes it easy to swap classes out at run-time. All you need to do is implement the interface and register your implementation with the class factory. This lets plugins stick their fingers into almost any aspect of Virtual Radar Server. Almost all of the interfaces that VRS uses are declared in VirtualRadar.Interface (which you'll remember you had to add a reference to when you created the plugin project). When VRS loads it calls a static function in each of its libraries, and those static functions tell the class library how to instantiate the interfaces that the library has classes for. If you want to completely swap out the default implementation of an interface then this is where you can tell VRS to ignore the default implementation and to use yours instead. Examples of plugins that do this are the Disable Audio plugin (which replaces the implementation of IAudio with a stub class that does nothing) and the Disable UPnP plugin (which replaces the implementation of IUniversalPluginAndPlayManager with a stub that does nothing). Another reason why you might want to swap out an implementation of an interface is if you want to extend the default implementation. The Feed Filter plugin implements a wrapper for the IListener implementation and uses that to extend the functionality of IListener, in this case to filter out messages from receivers. Our example will not swap out any of the VRS interfaces so our version is going to be empty. If you want to see examples of this function in use then take a look at the source for the FeedFilter, DisableAudio and DisableUPnP plugins on GitHub. |
Startup |
This is called fairly late on in the startup process. By this point the web site will be up, the feeds will be connecting, the log can be written, almost everything is good to go. Note that the function is called on a background thread so you cannot perform any GUI calls here. It is here that you should load your options (if you have any), inject yourself into the web site, hook events, start timers or do whatever setup you need to do to get the plugin running. To help with this the function is passed an object of type PluginStartupParameters. The source for this can be found on GitHub. Our example will display the local time and date in the plugin options list (just for something to do) so we have code in our sample to start a timer and periodically update Status and StatusDescription. |
GuiThreadStartup |
This is called after the plugins have started up. Unlike Startup it is not called on a background thread, it is called on the GUI thread. If your plugin needs to make calls from the GUI thread during startup (for example it needs to create a modeless window) then this is the place to do that. Our example doesn't need to do any GUI startup so the method is empty. Very few plugins need to do GUI startup. |
Shutdown |
This is called after the user has told VRS that they want to close the program. It is your opportunity to do any cleanup before the program closes, e.g. close database connections, flush caches, cleanly dispose of resources etc. Note that this function, like most other Plugin functions, is called on a background thread.
Plugins are shut down fairly early on in the shutdown process. At the point that In our example we just destroy the timer that we created in Startup. Technically we don't need to do that as the Timer will be destroyed by Windows when it cleans up the VRS process, but it's nice to be tidy. |
ShowWinFormsOptionsUI |
Unlike the other functions this one can be called more than once during the lifetime of the plugin. It is called every time the user clicks the Options button in the plugin list. If you are returning false from the HasOptions property then this function should never be called. Our plugin doesn't do anything so the method is empty. |
Sample
This is our sample Plugin.cs after all of the stubs have been fleshed out. All it does is show the date and time in the status fields on the plugin list.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using VirtualRadar.Interface; namespace SamplePlugin { public class Plugin : IPlugin { public bool HasOptions { get { return false; } } public string Id { get { return "sample.plugin"; } } public string Name { get { return "Sample Plugin"; } } public string PluginFolder { get; set; } public string Version { get { return "1.0"; } } private string _Status; public string Status { get { return _Status; } set { if(value != _Status) { _Status = value; OnStatusChanged(EventArgs.Empty); } } } private string _StatusDescription; public string StatusDescription { get { return _StatusDescription; } set { if(value != _StatusDescription) { _StatusDescription = value; OnStatusChanged(EventArgs.Empty); } } } public event EventHandler StatusChanged; private void OnStatusChanged(EventArgs args) { var statusChanged = StatusChanged; if(statusChanged != null) { statusChanged(this, args); } } // The timer that Startup will create and Shutdown will destroy private System.Timers.Timer _Timer; public void RegisterImplementations(InterfaceFactory.IClassFactory classFactory) { ; } public void Startup(PluginStartupParameters parameters) { _Timer = new System.Timers.Timer() { AutoReset = true, Interval = 500, // half a second Enabled = false, }; _Timer.Elapsed += Timer_Elapsed; _Timer.Enabled = true; } void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { var now = DateTime.Now; Status = String.Format("The time is now {0:T}", now); StatusDescription = String.Format("The date is now {0:d}", now); } public void GuiThreadStartup() { ; } public void ShowWinFormsOptionsUI() { ; } public void Shutdown() { _Timer.Dispose(); } } }
Running the Plugin
The moment of truth :) Press F5
in Visual Studio. If all goes well then Virtual Radar Server should start up
and when you click Tools | Plugins
you should see your plugin in the list with the status and
status description lines showing a clock updating in real time.
You should also be able to set a breakpoint in the plugin code and Visual Studio should break on it. To see this
happening go to the first line of the Timer_Elapsed function and press F9
to set a breakpoint. Visual Studio
should hit the breakpoint within 500ms (the interval that we've set on the timer). Press F9
again to remove
the breakpoint and then F5
to continue execution.
Troubleshooting
- Have you changed the assembly name in the project properties so that the assembly starts with VirtualRadar.Plugin.? If you haven't then VRS will ignore your DLL when it's looking for plugins to load.
- Do you have a manifest file, does it have the <PluginManifest> root node, does it have the same name as the DLL but with an extension of XML and has it configured to be copied to the output folder? Even if you don't have the MinimumVersion and MaximumVersion entries you still need to have the PluginManifest tag, otherwise VRS will ignore your plugin.
- Is your Plugin class public? VRS will only look for the IPlugin interface on public classes.
- Does your Plugin class implement the IPlugin interface? If it doesn't then VRS won't call it.
-
Is VRS reporting that it couldn't load your plugin? If so then check the log (
Tools | OpenVirtualRadarLog.txt
), the program will log plugin load errors in there. - If you are not targeting the beta version of VRS - when you created the project did you set it for .NET 3.5? If not then VRS won't be able to load it. You can change a .NET 4 project to 3.5 from the project properties screen. You will need to remove references to .NET 4 assemblies that can no longer be loaded. They will have a warning mark next to them in the references list.