It sounded fun, so I did some research into this. It's not a novice concept, so you'll have to understand some concepts before it all becomes really clear. It's not hard; I implemented the example application in about 10 minutes after reading half of a chapter in
CLR via C# about loading types. It does require you to be familiar with reflection, interface inheritance, pitfalls of application versioning, and loosely-coupled application architectures.
I'm not fooling with a web browser control, but I will talk you through developing a WinForms application that uses plugins that can interact with the form; I'm certain if you study my example you can figure out how to apply the technique to access a web browser control. You can ignore my advice and skip most of the suggestions I'm about to outline (really only the interfaces and the
PluginLoader class are vital), but I feel like the practices I'm discussing help make maintaining the application easier. It also might be a good idea to mention that there's a
Managed Extensibility Framework that might simplify all of this; I haven't studied it and I can't say.
You really need to split your application into three projects to do things in the best way possible. First, you need the application itself. Next, you need a class library that contains the interfaces for the plugins. Finally, you need a class library that actually contains plugins.
The big hurdle that I think makes this harder than it seems is the standard "everything in a form" approach to development makes supporting external functionality much more difficult. Sure, you can share references to the form and its controls or make methods public so that external components can access them, but this can become a nightmare over time. This creates a dependency between the plugin and your application, which means that in certain cases any time you recompile the application, you have to also recompile the plugins. This is why I suggest a class library that contains the interfaces; it's a kind of mediator between the application and the plugin, and it's the only thing that other projects depend upon. This also means that ideally, you will never change anything in the interface project once the application is stable; changing the interfaces means the other components might have to change.
Given this, the "everything in a form" approach would seem to encourage you to put your form in the interface library, since everything else accesses it. This is wrong; the form belongs in the application. The correct thing to do is to use a UI pattern that separates the logic of the form from the form itself; patterns to consider include Model-View-Controller and Passive View. I'm not going to do this in the example because it would do little more than make it harder to find the code that actually loads plugins. Keep this in mind, though.
Another concept to understand is the use of Interfaces so that you don't rely on having to know class types at compile time. If a class implements an Interface, you can use the interface methods even if you don't really know the type of the class you're working with. Consider this method:
Code:
Sub PrintItems(Of T)(ByVal itemCollection As IEnumerable(Of T))
itemCollection can be an array,
List(Of T),
Collection(Of T), or any other class that implements
IEnumerable(Of T). The method can call the
GetEnumerator() method on
itemCollection, because this method is guaranteed by the interface. Working with interfaces is powerful, and gives you great flexibility.
Finally, you need to have a basic knowledge of reflection. Most of the reflection types are in the
System.Reflection namespace. Reflection allows you to look at the types, methods, etc. in an assembly. You can load assemblies that aren't loaded, find the types inside of them, and create instances of those types. This is key to creating a plugin framework.
Now, for the example application. It simulates an application that lets you input search terms, select a search engine, then click a button to search for the terms with the selected search engine. The application puts the number of results that were returned in a text box on the form. You want to be able to add more search engines at any time, so the application will implement all search engine functionality as plugins that will be loaded at runtime; plugins are expected to be in a special plugins directory. You'll have to add the part that checks for new DLLs and downloads them yourself; that'd just clutter up this example. The example is in VS2008 format; if you need VS 2005 you can simply create new projects and add the files to them.
The
PluginInterfaces project is a good place to start. This class library is responsible for providing the interfaces that the application and plugins will use to talk to each other.
ISearchPlugin is the interface that any search plugin needs to implement. This is how the application knows how to talk to the plugin. It needs a method to execute a search, and a name that can be displayed to the users.
At this point, we have enough to discuss how the application works at a high level. When the application starts, the form needs to load all plugins and present a list of available search engines to the user. When the user clicks the form's button, the form will determine the selected search engine and use the appropriate
ISearchPlugin to perform the search. The form will display the results in the text box. There is intentionally no implementation inside of the
PluginInterfaces project: implementation can change so we want implementations to be either in the application or the plugins.
Now let's look at the
PluginDemo project. I separated the "load plugins" functionality into a
PluginLoader because it's a good practice and it helps show off the heart of the example. Note that I had to add a reference to the
PluginInterfaces project and add Imports statements to make the project work.
PluginLoader.GetPlugins() is where the magic happens. It looks for all DLL files in the appropriate directory and opens them. Then, it looks in the DLLs for public types that implement
ISearchPlugin. When it finds one, it uses
Activator.CreateInstance() to instantiate the class and store the instance in the list of plugins. When that's done, it returns the list of plugins. If you peek at
MainForm, you see that the
Load event handler loads plugins and exits if none were found, and the handler for the button click calls the appropriate
ISearchPlugin.PerformSearch. Easy.
This is useless without plugins, so let's look at the
Plugins project. For simplicity, the plugins I implemented don't really search using the indicated search engine; instead they return a somewhat random string. Note again that I had to add a reference to
PluginInterfaces and appropriate Imports statements to make things work. All of the plugin classes are the same (enough that I should have made a base class), so let's just discuss
GoogleSearch. When it's created, a random number generator (RNG) is initialized.
Name returns "Google".
PerformSearch() calculates a random number of search results and returns a string that indicates how many results were returned.
One small thing: if you want the application to work, make sure to go create the
plugins directory yourself and put the
Plugins project's dll into it. If you don't do this, the app will refuse to run. This also means that any time you change the
Plugins project and rebuild it, you'll have to make sure to copy the new files. If you're clever, you'll make a post-build task that does this for you so you don't have to worry about it.
I hope this sheds some light on how you might add plugin support to your application.