Extend your .NET Applications with Add-ons
Your application lacks a cool whizbang feature? Let other interested developers extend your application through add-ons. By opening up your app to additional functionality down the line, you can build a thriving community around your application, potentially even a marketplace!
In today's tutorial, we're going to learn just that!
Step 0 - Our Game Plan
Depending on which version of Visual Studio you're using, and the version of the framework you're targeting, some screenshots may look slightly different.
We're going to build a proof-of-concept application that, upon start-up, loads add-ons that it finds in one of its subdirectories. These add-ons are .NET assemblies that contain at least one class which implements a particular interface as defined by us. The concepts in this tutorial should be easily transferred into your existing applications without much hard work. Then we'll look at a more complete example utilizing UI components loaded from an add-on.
This tutorial was written using Microsoft Visual Studio 2010, in VB.NET targeting .NET Framework 4.0. The salient concepts of this tutorial should work in other CLR languages, from .NET Framework 1.1 upwards. There is some minor functionality which will not work in other CLR languages - but should be very easily ported - and some concepts like generics which obviously won't work in .NET 1.1 etc.
Note: This is an expert level tutorial for people happy with converting code between different CLR languages and versions of the .NET framework. It's more 'replacing your car engine' than 'how to drive'.
Step 1 - Initial Small Scale Implementation
Note: Your references may be different depending on which version of the .NET Framework you're targeting.
Let's start by implementing a small-scale version of our application. We will need three projects in our solution:
-
ConsoleAppA: A console application -
ClassLibA: A class library -
AddonA: A class library that will act as our add-on
Add a reference from both ConsoleAppA and and AddonA to ClassLibA. Solution explorer should then look as follows:
Creating the Add-on Interface
For a compiled class to be considered compatible, every add-on for our application will need to implement a specific interface. This interface will define the required properties and operations that the class must have so that our application can interact with the add-on without hitches. We could also use an abstract/MustInherit class as the basis for add-ons but in this example we'll use an interface.
Here is the code for our interface, which should be placed in a file called IApplicationModule within the ClassLibA class library.
1 |
|
2 |
Public Interface IApplicationModule |
3 |
|
4 |
ReadOnly Property Id As Guid |
5 |
ReadOnly Property Name As String |
6 |
|
7 |
Sub Initialise() |
8 |
|
9 |
End Interface |
The properties and methods you define in the interface are arbitrary. In this case,, I have defined two properties and one method, but in practice you can and should change these as required.
We're not going to use the Id or Name properties in our first example, but they're useful properties to implement, and you'd probably want these present if you're using add-ons in production.
Creating an Add-on
Now let's create the actual add-on. Again, for any class to be considered an add-on for our application, it needs to implement our interface - IApplicationModule.
Here is the code for our basic add-on, which should be placed in a file called MyAddonClass within the AddonA class library.
1 |
|
2 |
Imports ClassLibA |
3 |
|
4 |
Public Class MyAddonClass |
5 |
Implements IApplicationModule |
6 |
|
7 |
Public ReadOnly Property Id As System.Guid Implements ClassLibA.IApplicationModule.Id |
8 |
Get
|
9 |
Return New Guid("adb86b53-2207-488e-b0f3-ecd13eae4042") |
10 |
End Get |
11 |
End Property |
12 |
|
13 |
Public Sub Initialise() Implements ClassLibA.IApplicationModule.Initialise |
14 |
Console.WriteLine("MyAddonClass is starting up ...") |
15 |
'Perform start-up initialisation here ...
|
16 |
End Sub |
17 |
|
18 |
Public ReadOnly Property Name As String Implements ClassLibA.IApplicationModule.Name |
19 |
Get
|
20 |
Return "My first test add-on" |
21 |
End Get |
22 |
End Property |
23 |
End Class |
Step 2 - Finding Add-ons at Runtime
Next, we need a way to find add-ons for our application. In this example, we'll assume that an Addons folder has been created in the executable's directory. If you're testing this within Visual Studio, bear in mind the default project output directory for projects, namely ./bin/debug/, so you would need a ./bin/debug/Addons/ directory.
Time to Reflect
Place the TryLoadAssemblyReference method below into Module1 of ConsoleAppA. We investigate the loaded assembly through the use of Reflection. A pseudo-code walkthrough of its functionality is as follows:
- Try to load the given dllFilePath as a .NET assembly
- If we've successfully loaded the assembly, proceed
- For each module in the loaded assembly
- For each type in that module
- For each interface implemented by that type
- If that interface is our add-on interface (
IApplicationModule),then - Keep a record of that type. Finish searching.
- Finally, return any valid types found
1 |
|
2 |
Private Function TryLoadAssemblyReference(ByVal dllFilePath As String) As List(Of System.Type) |
3 |
Dim loadedAssembly As Assembly |
4 |
Dim listOfModules As New List(Of System.Type) |
5 |
Try
|
6 |
loadedAssembly = Assembly.LoadFile(dllFilePath) |
7 |
Catch ex As Exception |
8 |
End Try |
9 |
If loadedAssembly IsNot Nothing Then |
10 |
For Each assemblyModule In loadedAssembly.GetModules |
11 |
For Each moduleType In assemblyModule.GetTypes() |
12 |
For Each interfaceImplemented In moduleType.GetInterfaces() |
13 |
If interfaceImplemented.FullName = "ClassLibA.IApplicationModule" Then |
14 |
listOfModules.Add(moduleType) |
15 |
End If |
16 |
Next
|
17 |
Next
|
18 |
Next
|
19 |
End If |
20 |
Return listOfModules |
21 |
End Function |
Firing up our Add-ons
Finally, we can now load a compiled class that implements our interface into memory. But we don't have any code to find, instantiate, or make method calls on those classes. Next we'll put together some code that does just that.
The first part is to find all files which might be add-ons, as below. The code performs some relatively straightforward file searching, and for each DLL file found, attempts to load all of the valid types from that assembly. DLLs discovered under the Addons folder aren't necessarily add-ons - they could simply contain extra functionality required by an add-on, but not actually be an add-on per se.
1 |
|
2 |
Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath |
3 |
Dim addonsRootDirectory As String = currentApplicationDirectory & "\Addons\" |
4 |
Dim addonsLoaded As New List(Of System.Type) |
5 |
|
6 |
If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then |
7 |
Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, "*.dll") |
8 |
For Each dllFile In dllFilesFound |
9 |
Dim modulesFound = TryLoadAssemblyReference(dllFile) |
10 |
addonsLoaded.AddRange(modulesFound) |
11 |
Next
|
12 |
End If |
Next we need to do something each valid type we've found. The code below will create a new instance of the type, type the type, call the Initialise() method (which is just an arbitrary method we defined in our interface), and then keep a reference to that instantiated type in a module level list.
1 |
|
2 |
If addonsLoaded.Count > 0 Then |
3 |
For Each addonToInstantiate In addonsLoaded |
4 |
Dim thisInstance = Activator.CreateInstance(addonToInstantiate) |
5 |
Dim thisTypedInstance = CType(thisInstance, ClassLibA.IApplicationModule) |
6 |
thisTypedInstance.Initialise() |
7 |
m_addonInstances.Add(thisInstance) |
8 |
Next
|
9 |
End If |
Putting it all together, our console application should begin to look something like so:
1 |
|
2 |
Imports System.Reflection |
3 |
|
4 |
Module Module1 |
5 |
|
6 |
Private m_addonInstances As New List(Of ClassLibA.IApplicationModule) |
7 |
|
8 |
Sub Main() |
9 |
LoadAdditionalModules() |
10 |
|
11 |
Console.WriteLine('Finished loading modules ...') |
12 |
Console.ReadLine() |
13 |
End Sub |
14 |
|
15 |
Private Sub LoadAdditionalModules() |
16 |
Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath |
17 |
Dim addonsRootDirectory As String = currentApplicationDirectory & '\Addons\' |
18 |
Dim addonsLoaded As New List(Of System.Type) |
19 |
|
20 |
If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then |
21 |
Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, "*.dll") |
22 |
For Each dllFile In dllFilesFound |
23 |
Dim modulesFound = TryLoadAssemblyReference(dllFile) |
24 |
addonsLoaded.AddRange(modulesFound) |
25 |
Next
|
26 |
End If |
27 |
|
28 |
If addonsLoaded.Count > 0 Then |
29 |
For Each addonToInstantiate In addonsLoaded |
30 |
Dim thisInstance = Activator.CreateInstance(addonToInstantiate) |
31 |
Dim thisTypedInstance = CType(thisInstance, ClassLibA.IApplicationModule) |
32 |
thisTypedInstance.Initialise() |
33 |
m_addonInstances.Add(thisInstance) |
34 |
Next
|
35 |
End If |
36 |
End Sub |
37 |
|
38 |
Private Function TryLoadAssemblyReference(ByVal dllFilePath As String) As List(Of System.Type) |
39 |
Dim loadedAssembly As Assembly |
40 |
Dim listOfModules As New List(Of System.Type) |
41 |
Try
|
42 |
loadedAssembly = Assembly.LoadFile(dllFilePath) |
43 |
Catch ex As Exception |
44 |
End Try |
45 |
If loadedAssembly IsNot Nothing Then |
46 |
For Each assemblyModule In loadedAssembly.GetModules |
47 |
For Each moduleType In assemblyModule.GetTypes() |
48 |
For Each interfaceImplemented In moduleType.GetInterfaces() |
49 |
If interfaceImplemented.FullName = 'ClassLibA.IApplicationModule' Then |
50 |
listOfModules.Add(moduleType) |
51 |
End If |
52 |
Next
|
53 |
Next
|
54 |
Next
|
55 |
End If |
56 |
Return listOfModules |
57 |
End Function |
58 |
End Module |
Trial Run
At this point we should be able to perform a full build of our solution. When your files have been built, copy the compiled assembly output of the AddonA project into the Addons folder of ConsoleAppA. The Addons folder should be created under the /bin/debug/ folder. On my machine using Visual Studio 2010, default project locations and a solution called "DotNetAddons" the folder is here:
C:\Users\jplenderleith\Documents\Visual Studio 2010\Projects\DotNetAddons\ConsoleAppA\bin\Debug\Addons
We should see the following output when we run the code is run:
1 |
|
2 |
MyAddonClass is starting up ... |
3 |
Finished loading modules ... |
As it stands it's not flashy or impressive but it does demonstrate a key piece of functionality, namely that at runtime we can pick up an assembly and execute code within that assembly without having any existing knowledge about that assembly. This is the foundation of building more complex add-ons.
Step 3 - Integrating with a User Interface
Next, we'll take a look at building a couple of add-ons to provide additional functionality within a Windows Forms application.
Similar to the existing solution, create a new solution with the following projects:
-
WinFormsAppA: A Windows Forms application -
ClassLibA: A class library -
UIAddonA: A class library that will house a couple of add-ons with UI components
Similar to our previous solution, both the WinFormsAppA and UIAddonA projects should have references to the ClassLibA. The UIAddonA project will also need a reference to the System.Windows.Forms for access to functionality.
The Windows Forms Application
We'll make a quick and simple UI for our application consisting of a MenuStrip and a DataGridView. The MenuStrip control should have three MenuItems
- File
- Add-ons
- Help
Dock the DataGridView to its parent container - the form itself. We'll put together some code to display some mock data in our grid. The Add-ons MenuItem will be populated by any add-ons that have been loaded on application start-up.
Here's a screenshot of the application at this point:
Let's write some code to give the main form of WinFormsAppA some functionality. When the form loads it'll call the LoadAddons() method which is currently empty - we'll fill that in later. Then we generate some sample employee data to which we bind our DataGridView.
1 |
|
2 |
Public Class Form1 |
3 |
|
4 |
Private m_testDataSource As DataSet |
5 |
Private m_graphicalAddons As List(Of System.Type) |
6 |
|
7 |
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load |
8 |
LoadAddons() |
9 |
m_testDataSource = GenerateTestDataSet() |
10 |
With DataGridView1 |
11 |
.AutoGenerateColumns = True |
12 |
.AutoResizeColumns() |
13 |
.DataSource = m_testDataSource.Tables(0) |
14 |
End With |
15 |
End Sub |
16 |
|
17 |
Private Sub LoadAddons() |
18 |
|
19 |
End Sub |
20 |
|
21 |
Private Function GenerateTestDataSet() As DataSet |
22 |
Dim newDataSet As New DataSet |
23 |
Dim newDataTable As New DataTable |
24 |
Dim firstNames = {"Mary", "John", "Paul", "Justyna", "Michelle", "Andrew", "Michael"} |
25 |
Dim lastNames = {"O'Reilly", "Murphy", "Simons", "Kelly", "Gates", "Power"} |
26 |
Dim deptNames = {"Marketing", "Sales", "Technical", "Secretarial", "Security"} |
27 |
|
28 |
With newDataTable |
29 |
.Columns.Add("EmployeeId", GetType(Integer)) |
30 |
.Columns.Add("Name", GetType(String)) |
31 |
.Columns.Add("IsManager", GetType(Boolean)) |
32 |
.Columns.Add("Department", GetType(String)) |
33 |
End With |
34 |
|
35 |
For i = 1 To 100 |
36 |
Dim newDataRow As DataRow = newDataTable.NewRow() |
37 |
With newDataRow |
38 |
.Item("EmployeeId") = i |
39 |
.Item("Name") = firstNames(i Mod firstNames.Count) & " " & lastNames(i Mod lastNames.Count) |
40 |
.Item("IsManager") = ((i Mod 20) = 0) |
41 |
.Item("Department") = deptNames(i Mod deptNames.Count) |
42 |
End With |
43 |
newDataTable.Rows.Add(newDataRow) |
44 |
Next
|
45 |
|
46 |
newDataSet.Tables.Add(newDataTable) |
47 |
Return newDataSet |
48 |
End Function |
49 |
|
50 |
End Class |
When we run the application it should look like so:


We'll come back to our empty LoadAddons() after we've defined the add-on interfaces.
Add-on Interface
Let's define our add-on interface. UI components will be listed under the Add-ons MenuStrip, and will open up a windows form that has access to the data to which our DataGrid is bound. To the ClassLibA project add a new interface file called IGraphicalAddon and paste in the following code into the file:
1 |
|
2 |
Public Interface IGraphicalAddon |
3 |
|
4 |
ReadOnly Property Name As String |
5 |
WriteOnly Property DataSource As DataSet |
6 |
Sub OnClick(ByVal sender As Object, ByVal e As System.EventArgs) |
7 |
|
8 |
End Interface |
- We have a
Nameproperty, which is pretty self-explanatory. - The
DataSourceproperly is used to 'feed' data to this add-on. - The
OnClickmethod, which will be used as the handler when users click on our add-on's entry in the Addons menu.
Loading the Add-ons
Add a class library called AddonLoader to the ClassLibA assembly project, with the following code:
1 |
|
2 |
Imports System.Reflection |
3 |
|
4 |
Public Class AddonLoader |
5 |
Public Enum AddonType |
6 |
IGraphicalAddon = 10 |
7 |
ISomeOtherAddonType2 = 20 |
8 |
ISomeOtherAddonType3 = 30 |
9 |
End Enum |
10 |
|
11 |
Public Function GetAddonsByType(ByVal addonType As AddonType) As List(Of System.Type) |
12 |
Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath |
13 |
Dim addonsRootDirectory As String = currentApplicationDirectory & "\Addons\" |
14 |
Dim loadedAssembly As Assembly |
15 |
Dim listOfModules As New List(Of System.Type) |
16 |
|
17 |
If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then |
18 |
Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, "*.dll") |
19 |
For Each dllFile In dllFilesFound |
20 |
|
21 |
Try
|
22 |
loadedAssembly = Assembly.LoadFile(dllFile) |
23 |
Catch ex As Exception |
24 |
End Try |
25 |
If loadedAssembly IsNot Nothing Then |
26 |
For Each assemblyModule In loadedAssembly.GetModules |
27 |
For Each moduleType In assemblyModule.GetTypes() |
28 |
For Each interfaceImplemented In moduleType.GetInterfaces() |
29 |
If interfaceImplemented.Name = addonType.ToString Then |
30 |
listOfModules.Add(moduleType) |
31 |
End If |
32 |
Next
|
33 |
Next
|
34 |
Next
|
35 |
End If |
36 |
|
37 |
Next
|
38 |
End If |
39 |
|
40 |
Return listOfModules |
41 |
End Function |
42 |
End Class |
You could use code similar to this to restrict add-on developers to build only specific types of add-ons
In this class, we've provided a way to load different types of add-ons. You could use code similar to this to restrict add-on developers to build only specific types of add-ons, or to at least categorise them. In our example, we'll just be using the first add-on type declared, namely IGraphicalAddon.
If we had other add-on types in our project, for example to enhance reporting functionality or provide some useful keyboard shortcuts, then we don't want them listed in our Add-ons menu, as there's no point in someone being able to click on that add-on.
Having said that though, it may be desirable to add a reference to all add-ons under some menubar, click on which would cause the add-on's options form to be displayed. This however is beyond of the scope of this tutorial.
The Graphical Add-on
Let's build the actual UI add-on. To the UIAddonA assembly project add the following two files; a class called UIReportAddon1 and a windows form called UIReportAddon1Form. The UIReportAddon1 class will contain the following code:
1 |
|
2 |
Imports ClassLibA |
3 |
|
4 |
Public Class UIReportAddon1 |
5 |
Implements ClassLibA.IGraphicalAddon |
6 |
|
7 |
Private _dataSource As DataSet |
8 |
Public WriteOnly Property DataSource As System.Data.DataSet Implements IGraphicalAddon.DataSource |
9 |
Set(ByVal value As System.Data.DataSet) |
10 |
_dataSource = value |
11 |
End Set |
12 |
End Property |
13 |
|
14 |
Public ReadOnly Property Name As String Implements IGraphicalAddon.Name |
15 |
Get
|
16 |
Return "Managers Report" |
17 |
End Get |
18 |
End Property |
19 |
|
20 |
Public Sub OnClick(ByVal sender As Object, ByVal e As System.EventArgs) Implements IGraphicalAddon.OnClick |
21 |
Dim newUiReportingAddonForm As New UIReportAddon1Form |
22 |
newUiReportingAddonForm.SetData(_dataSource) |
23 |
newUiReportingAddonForm.ShowDialog() |
24 |
End Sub |
25 |
End Class |
To UIReportAddon1Form add a DataGridView that's docked within its parent container. Then add the following code:
1 |
|
2 |
Public Class UIReportAddon1Form |
3 |
|
4 |
Private m_providedDataSource As DataSet |
5 |
|
6 |
Public Sub SetData(ByVal DataSource As System.Data.DataSet) |
7 |
m_providedDataSource = DataSource |
8 |
FilterAndShowData() |
9 |
End Sub |
10 |
|
11 |
Private Sub FilterAndShowData() |
12 |
Dim managers = m_providedDataSource.Tables(0).Select("IsManager = True") |
13 |
Dim newDataTable = m_providedDataSource.Tables(0).Clone |
14 |
For Each dr In managers |
15 |
newDataTable.ImportRow(dr) |
16 |
Next
|
17 |
DataGridView1.DataSource = newDataTable |
18 |
End Sub |
19 |
|
20 |
Private Sub UIReportAddon1Form_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load |
21 |
Text = "List of managers" |
22 |
End Sub |
23 |
End Class |
We're simply providing the means to send data to the form, and then let it decide what to do with said data. In this case, we're going to perform a Select() on a DataTable, retrieving back a list of all employees who are managers.
Back to our Main UI
Now it's time to complete our unfinished LoadAddons() method in our WinFormsAppA project. This code will instruct the GetAddonsByType method of ClassLibA.AddonLoader to find a particular type of add-on - in this case IGraphicalAddon add-ons.
For each add-on found, the following actions are performed
- Create a new instance of the add-on
- Type the add-on instance to IGraphicalAddon
- Populate its DataSource property
- Add the add-on to the Add-ons menu item
- Add a handler for that menu item's Click event
1 |
|
2 |
Private Sub LoadAddons() |
3 |
Dim loader As New ClassLibA.AddonLoader |
4 |
Dim addonsLoaded = loader.GetAddonsByType(ClassLibA.AddonLoader.AddonType.IGraphicalAddon) |
5 |
|
6 |
For Each graphicalAddon In addonsLoaded |
7 |
Dim thisInstance = Activator.CreateInstance(graphicalAddon) |
8 |
Dim typedAddon = CType(thisInstance, ClassLibA.IGraphicalAddon) |
9 |
typedAddon.DataSource = m_testDataSource |
10 |
Dim newlyAddedToolstripItem = AddonsToolStripMenuItem.DropDownItems.Add(typedAddon.Name) |
11 |
AddHandler newlyAddedToolstripItem.Click, AddressOf typedAddon.OnClick |
12 |
Next
|
13 |
End Sub |
Testing our UI
Ensure you can perform a full successful compile of the solution. Copy the UIAddonA.dll from the output debug folder of the UIAddonA project into the /bin/debug/Addons/ folder of the WinFormsAppA project. When you run the project, you should see a grid of data, as before. If you click into the Add-ons menu however you should now see a reference to the newly created add-on.


When we click on the Managers Report menu item, this will case a method call to the OnClick method implemented in our add-on, which will cause UIReportAddon1Form to appear, as below.


Wrap Up
We've seen two examples of building an application which can pickup and run add-ons as required. As I mentioned earlier, it should be easy enough to integrate this type of functionality into existing applications. The question of how much control you'll need (or want) to give third party developers is another matter entirely.
I hope you had fun with this tutorial! Thank you so much for reading!



