Extensibility Modules
Extensibility modules let you consolidate your plug-ins into a centralized class, giving all your developers one place to go to figure out what kind of extensibility is available to them for a given application.
If you're familiar with the concept of HttpModules, you're going to feel right at home with this subject.
The preceding section showed you how to write an interface for each type of plug-in; the signature of the interface member(s) identifies the data that the plug-in class needs for its processing. Extensibility modules require a little more work, but it's a lot more organized. The example used here shows you how to do this by writing what would otherwise be three different plug-ins. One serves the same purpose as the one in the plug-ins example, a "post log" plug-in, called "post processing." Another will be a "pre-processing" plug-in, and a third is a "check data source" plug-in.
The points of extensibility I'll insert into my client will be as follows:
- The "Check Data Source" plug-in process will run immediately following the GetSource method in my provider, and will determine whether processing should continue.
- The "Pre-Process" plug-in process will run just before you "log" the data.
- The "Post-Process" plug-in process will run immediately after you "log" the data.
The cool thing here is that I'm going to do this using just one interface. But because of that, I'm going to need an EventArgs-based class for each of my three plug-in scenarios. Like any conventional EventArgs-based class, each of class will contain the data I need to get from the client app into the "plug-in" process code and back.
' In VB:
Public Class CheckDataSourceEventArgs
Inherits CancelEventArgs
Public Sub New(ByVal source As String)
_Source = source
End Sub
Protected _Source As String
Public Property Source() As String
Get
Return _Source
End Get
Set(ByVal value As String)
_Source = value
End Set
End Property
End Class
Public Class PreProcessDataEventArgs
Inherits EventArgs
Public Sub New(ByVal data As String)
_Data = data
End Sub
Protected _Data As String
Public Property Data() As String
Get
Return _Data
End Get
Set(ByVal value As String)
_Data = value
End Set
End Property
End Class
Public Class PostProcessDataEventArgs
Inherits PreProcessDataEventArgs
Public Sub New(ByVal data As String)
MyBase.New(data)
End Sub
End Class
// In C#:
public class CheckDataSourceEventArgs :
CancelEventArgs
{
public CheckDataSourceEventArgs(
string source)
{
_Source = source;
}
protected string _Source;
public string Source
{
get
{
return _Source;
}
set
{
_Source = value;
}
}
}
public class PreProcessDataEventArgs : EventArgs
{
public PreProcessDataEventArgs(string data)
{
_Data = data;
}
protected string _Data;
public string Data
{
get
{
return _Data;
}
set
{
_Data = value;
}
}
}
public class PostProcessDataEventArgs :
PreProcessDataEventArgs
{
public PostProcessDataEventArgs(
string data) : base(data)
{
}
}
As you can see, the PostProcessDataEventArgs class is going to need the same data as the PreProcessDataEventArgs class. To make things simpler, I'm just inheriting one from the other.
With the EventArgs class to carry information to and from each plug-in process in place, here's a class that will use them, called ModuleEvents, which contains a set of properties—one for each plug-in process I want to define. Each property type is a delegate that defines a signature of the corresponding EventArgs-based class. Each property wraps a member variable of the same delegate type. Does that sound confusing enough? Take a look at the code and it should clear things up.
First, you need the delegate types, but fortunately, you need only one, thanks to the wonderful world of .NET Generics:
' In VB:
Public Delegate Sub
AcmeModuleDelegate(Of T) _
(ByVal e As T)
// In C#:
public delegate void
AcmeModuleDelegate<T>(T e);
Now the property types can all use the same delegate type, but each can provide a different value for the generic type. To create the member variables:
' In VB:
Private _CheckDataSource As _
AcmeModuleDelegate( _
Of CheckDataSourceEventArgs)
Private _PreProcessData As _
AcmeModuleDelegate( _
Of PreProcessDataEventArgs)
Private _PostProcessData As _
AcmeModuleDelegate( _
Of PostProcessDataEventArgs)
// In C#:
private
AcmeModuleDelegate
<CheckDataSourceEventArgs>
_CheckDataSource;
private
AcmeModuleDelegate
<PreProcessDataEventArgs>
_PreProcessData;
private
AcmeModuleDelegate
<PostProcessDataEventArgs>
_PostProcessData;
Now you need to define the public properties that expose the member variables. You can see that in
Listing 4.
Now, an instance of ModuleEvents will contain properties that are delegates. As you know, a delegate can have one or more function pointers in its invocation list.
You still need the interface that you'll use to write plug-ins later. As I said earlier, instead of creating a different interface for each plug-in, I'm going to have only one, which looks like this:
' In VB:
Public Interface IAcmeModule
Sub Initialize(ByVal events As
ModuleEvents)
End Interface
// In C#:
public interface IAcmeModule
{
void Initialize(ModuleEvents events);
}
Notice that there's only one method,
Initialize, which receives a ModuleEvents argument type, which I just created. Now, the best way to continue is to write a plug-in using this new model.
From what you've seen so far, developers have only one interface they need to know about to extend this application. So I'm going to write a new class in a new project called
NotificationModule. This class will perform the same process as the
EmailProcessing plug-in described earlier; it will implement the IAcmeModule interface and implement the
Initialize method. The
Initialize method, accesses the events argument, whose properties are delegates. The IntelliSense itself will tell me what my possible extensibility points are. Because each one is a delegate, I can just wire a method in this class to that delegate, effectively adding to its invocation list.
' In VB:
Public Class NotificationModule
Implements IAcmeModule
Public Sub Initialize(ByVal events As _
Core.ModuleEvents) Implements _
Core.IAcmeModule.Initialize
events.PostProcessData = _
New AcmeModuleDelegate( _
Of PostProcessDataEventArgs)( _
AddressOf events_PostProcessData)
End Sub
Private Sub events_PostProcessData( _
ByVal e As PostProcessDataEventArgs)
' perform e-mail functionality
' with processed data
End Sub
End Class
// In C#:
public class NotificationModule :
IAcmeModule
{
void IAcmeModule.Initialize(
ModuleEvents events)
{
events.PostProcessData +=
events_PostProcessData;
}
void events_PostProcessData(
PostProcessDataEventArgs e)
{
// perform e-mailing of processed data
}
}
As you can see, you can decide within the module class itself which extensibility point you want to tap into.