The idea is very simple. You almost got it, should just think about it.
The kind of late binding you use is of the level of plug-in (your "registered" DLL) and the plug-in architecture. You need to look at it as at the whole architecture, and it all will be clear to you. The think is: the host application is never totally agnostic the the plug-in but is agnostic to the implementation detail. The concrete type of event arguments is not one of them. Also, for assignment of event arguments, classical OOP principles apply. And I appreciate you idea on "late binding": this has a deep analogy in late binding as it is understood in OOP.
Plug-in architecture is a pretty interesting paradigm, unlike client-server and other rather primitive concepts. You have a host application and some plug-ins. You need to have some plug-in API to recognize some classes and members throw reflection, to use them in the host application. In general case, the host application also provides some plug-in API and its implementations to the plug-ins, so a plug-in and a host application implement their plug-in related interfaces and pass to each other the interface references to those implementation.
Now, let's remember that there is no such thing as a miracle. The interfaces are declared somewhere. It can be a separate assembly referenced by both the host and all plug-in implementations, but it can fully reside in the host assembly. (It's important to understand that when you load or reference some assembly, the difference between "EXE" and "DLL" is totally insignificant; those are nothing more then file naming conventions; the files can have any names; you can always reference a host assembly by plug-in.) So, the event argument type should be defined in the same place where the plug-in interfaces are, so both host and plug-in assemblies could use it (quite apparently). These and only these event argument types can serve as a
design-time type on the host part. As to the
run-time type… I'll consider it later, but it depends on what you want to do with them. But before we go there, let me tell you that the reflection approach Philip Stuyck suggested won't give you much. With reflection, you can discover additional members, but how would you know its semantic meaning?
So, let's finally go back to the plug-in derived event argument types. You can apply the principle of classic OOP here. You need to isolate "what to do with it" for some member from "how to do it" which is known to the plug-in. However, you need to stay in the framework of this concept. In particular, you should not try to move derived types back to the plug-in interface, tag the types and try down-casting.
Consider this. In the plug-in interfaces:
abstract class PluginEventArgs : EventArgs {
void KnowWhatToDoWithIt() {
Info = KnowHowToDoIt();
}
protected abstract string KnowHowToDoIt();
internal string Info;
}
In plug-in:
class ThisPlugInEventArgs : PluginEventArgs {
protected override string KnowHowToDoIt() {
return
}
MyType someField;
}
Very roughly — just to give an idea.
Good luck,
—SA