.NET 2 the Max
Create Extensible .NET Apps
Take advantage of plug-ins to create highly extensible and highly customizable Windows Forms applications that serve your users' needs better.
It's nice to create applications that you can customize easily for different customers. At the same time, you don't want to disperse hundreds of If statements and other conditional blocks just to account for each and every possible user. In fact, you shouldn't do this, for at least two reasons: First, it would force you to recompile and redeploy your executables for each minor change; second, it would make source code maintenance a nightmare.
Creating customized applications means you must plan well in advance the ways in which you want your application to be extensible. For example, you must decide whether an extension can change the structure of existing forms or replace them with a completely new form, or both. You must also establish whether an extension can intervene during operations, such as when loading and saving data, and so forth.
An effective approach to creating extensible and customizable apps is to use plug-in DLLs that leverage many programming techniques such as custom attributes, reflection, and interfaces. Following the guidelines in this article will let you change the appearance and the behavior of a WinForms program by simply asking your customers to copy a DLL into a given directory under the app's directory tree.
An extensible app consists of three types of executable files: the main app's EXE file, the ExtensibilityLib.dll file, and zero or more plug-in DLLs (see Figure 1). The ExtensibilityLib.dll defines the all-important FormExtender custom attribute, which you use to mark a plug-in class that extends a given form in the main app. This DLL also defines the FormExtenderManager type, which exposes methods the main app and your plug-ins use.
The mechanism works roughly like this: When the main application creates a new form, it should pass the form object to the FormExtenderManager.NotifyFormCreation method. The first time this method is invoked, it calls the InitializePlugins method, which scans all the DLLs stored in the application's main directory and keeps track of all the types that are marked with a FormExtender attribute.
The NotifyFormCreation method creates an instance of all the types (stored in the plug-in DLLs) that want to extend the form currently shown. The code in the plug-in assembly receives the instance of the form being created, so it can change its properties, add or remove controls, create event handlers for existing controls, and so on. For example, assume you want to create an extremely simple plug-in that receives the form object in the class constructor and changes its background color to red:
<ExtensibilityLib.FormExtender(_
"MainApp.Form1")> _
Public Class Form1_Extender
Sub New(ByVal frm As Form)
frm.BackColor = Color.Red
End Sub
End Class
Extend a Form Class
A plug-in can extend a given form class, as well as all the classes that inherit from that class, by setting the attribute's IncludeInherited property to True. For example, this code lets you change the background colors of all the forms the main application uses:
<ExtensibilityLib.FormExtender( _
"System.Windows.Forms.Form", _
IncludeInherited:=True)> _
Public Class Form1_Extender
Sub New(ByVal frm As Form)
frm.BackColor = Color.Red
End Sub
End Class
The main application lets plug-ins participate in the form creation process in one of two ways. First, the main application can use the FormExtenderManager.CreateForm method to instantiate its own forms. Second, it can call the FormExtenderManager.NotifyFormCreation method from inside a form's constructor or the Form_Load event handler:
' inside an extensible form
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Let extenders know that we have a new form
FormExtenderManager.NotifyFormCreation(Me)
End Sub
Optionally, you can inherit your form from the ExtensibleFormBase class defined in ExtensibilityLib, which invokes the method call for you (see Figure 2). Note that you don't need to change the code that creates and uses the form, regardless of whether you include the call to NotifyFormCreation in your forms manually or have them inherit from ExtensibleFormBase. This means using the first approach has a minimal impact on the way you work with forms in your application.
The second technique you can use to activate plug-ins requires that you create your form by invoking the CreateForm method rather than by using the New operator. For example, you'd need to replace all statements that read like this:
Dim frm As New Form1
frm.Show()
Instead, you'd have to adopt this approach:
Dim frm As Form = _
ExtensibilityFormManager.CreateForm( _
GetType(Form1))
frm.Show()
You must also ensure that you can extend the startup form. Do this by setting the main app's startup object to Sub Main and adding a module that contains this code:
Sub Main()
Dim frm As Form = _
FormExtenderManager.CreateForm( _
GetType(MainForm))
Application.Run(frm)
End Sub
The approach based on the CreateForm method requires more code changes and is less transparent to the programmer, but has a great advantage over the technique based on the NotifyFormCreation method: It lets a plug-in replace (as opposed to modify) the requested form with a completely different form. You can achieve this result by applying the FormExtender attribute to a form class and ensuring that you pass True in its second argument:
' inside the plug-in DLL
<FormExtender("MainApp.MainForm", True)> _
Public Class MainForm_Replacement
Inherits System.Windows.Forms.Form
' ...
End Class
I recommend that you don't mix the two techniques in the same application. At a minimum, you should never use the CreateForm method to instantiate a form whose constructor invokes the NotifyFormCreation method, because this action causes the plug-in type to be instantiated twice. It's OK to use the CreateForm method to instantiate a form that inherits from ExtensibleFormBase because the method recognizes the form's base class and doesn't perform the notification directly.
Add a Control to a Form
Next, enable a plug-in to add a control to a form and react to events that this control raises (see Figure 3). The code is simple, though you might need to employ some guess work to size and place the control correctly on the existing form:
< FormExtender("MainApp.Form1")> _
Public Class Form1_Extender
Dim WithEvents btnShow As Button
Sub New(ByVal frm As Form)
' create a button
btnShow = New Button
btnShow.Text = "Do something!"
btnShow.Size = New Size(100, 32)
btnShow.Location = _
New Point(200, 100)
frm.Controls.Add(btnShow)
End Sub
Private Sub btnShow_Click(ByVal _
sender As Object, ByVal e As _
EventArgs) Handles btnShow.Click
' do something here!
End Sub
End Class
A plug-in can also access existing controls and intercept their events, but this task is more difficult than you might expect. The problem: The plug-in DLL can't reference a form type buried in the MainApp.exe assembly directly; therefore, all references to existing controls must occur in a sort of late-bound fashion, by enumerating all the elements in the form's Controls collection until the searched control is found. The ExtensibleFormManager class exposes a FindControl method that does this job for you (and looks inside container controls correctly). For example, this code enables a plug-in to change the caption of a button and run a piece of code when the user clicks on it:
Sub New(ByVal frm As Form)
' get a reference to btnOK
Dim btnOK As Button = DirectCast( _
FormExtenderManager.FindControl( _
frm, "btnOK"), Button)
' change its caption
btnOK.Text = "Save"
' create an event handler
AddHandler btnOK.Click, AddressOf _
OKButton_Click
End Sub
Private Sub OKButton_Click(ByVal _
sender As Object, ByVal e As _
EventArgs)
' save data here
End Sub
Avoid both the guesswork and the need to get a reference to existing controls when using the FindControl method by applying a simple trick. Don't define all the app's forms defined in the main EXE file; rather, define them in a separate DLL (named MainApp_Forms.dll in the demo program). More precisely, you should implement the entire app as a DLL and use the main EXE file only to display a startup form.
If you already use Sub Main as your app's entry point, the whole operation is as simple as moving all form files from one project to another (you can use drag and drop inside the Solution Explorer window) and changing the MainApp_Forms' root namespace to "MainApp." Changing the root namespace means you don't need to change any FormExtender attributes inside your plug-ins. The entire operation should take one minute or two.
Keeping your forms in a DLL instead of the main EXE file lets you achieve two important benefits. First, plug-in projects can reference this DLL and can access the original forms in a strongly typed manner:
<FormExtender("MainApp.Form1")> _
Public Class Form1_Extender
' notice that argument is strong-typed
Sub New(ByVal frm As MainApp.Form1)
frm.btnOk.Text = "Save"
End Sub
End Class
Secondand more importantlya plug-in project can replace a form with a form that inherits from the original form. This approach simplifies greatly the task of making extensive changes to the user interface. For example, assume that MainApp_Forms.dll contains a data-entry form named CustomersForm that you create using the Data Form wizard against the Customers table in SQL Server's Northwind database (see Figure 4). Also, assume that one of your users later asks you to display the list of orders related to each customer in the same form (see Figure 5).
The simplest way to satisfy your users' expectations is to create a new form named CustomerForm_Replacement in the plug-in project. This form inherits from the original CustomersForm type, and you mark it with a proper FormExtender attribute:
<FormExtender("MainApp.CustomersForm", _
True)> Public Class _
CustomerForm_Replacement
Inherits MainApp.CustomersForm
' ...
End Class
You can stretch the inherited form and move its controls to accommodate a DataGrid control, which is bound to the Customers_Orders relation in the objNorthwindDataSet object. You also need to add an OleDbDataAdapter object that fills the Orders table in the data set. Thanks to form inheritance, you need only a couple of event handlers to account for the new table:
Private Sub btnLoad_Click(ByVal sender _
As Object, ByVal e As EventArgs) _
Handles btnLoad.Click
Me.OleDbDataAdapter2.Fill( _
Me.objNorthwindDataSet.Orders)
End Sub
Private Sub btnUpdate_Click(ByVal _
sender As Object, ByVal e As _
EventArgs) Handles btnUpdate.Click
Me.OleDbDataAdapter2.Update( _
Me.objNorthwindDataSet.Orders)
End Sub
Interestingly, the code in the main application continues to work correctly even if it uses a strong-typed variable typed (instead of a generic Form variable), because the form in the plug-in project derives from the original form and therefore can be assigned to a variable typed as the original form:
Dim frm As CustomersForm = DirectCast( _
ExtensibilityFormManager.CreateForm( _
GetType(CustomersForm))
frm.Show()
This article touches on only a handful of what's possible. It's important to keep in mind that different kinds of applications can require (often radically) different approaches, so you should consider this article more as an introduction to the subject, rather than a by-the-numbers guide applicable to all circumstances. For more details, you can browse the source code of the companion sample application. Finally, be mindful of the security context your app will operate in, and take precautions to ensure no one attempts to piggyback malicious code onto the DLL structure of your app (see the sidebars, "Make Your Plug-In Architecture Secure" and "Look for More at .Net2TheMax").
About the Author
Francesco Balena has authored several programming books, including Programming Microsoft Visual Basic .NET Version 2003 [Microsoft Press]. He speaks regularly at VSLive! and other conferences, founded the .Net2TheMax family of sites, and is the principal of Code Architects Srl, an Italian software company that offers training and programming tools for .NET developers. Reach him at [email protected].