I am writing a physics simulator with a controler as main program and the physical systems as classes in dlls. To compile the main program I must have the systems in my reference list. Each time I add a new system, I must compile the main program anew.
Now I would like to be able to add new systems without recompiling the main program each time. To do so, 2 problems must be solved:
1. The new systems must be linked at runtime. Is that possible?
2. When I compile the main program I still don't know the name of the classes, the additional dlls will implement. Let's say that the first point is solved. At runtime I look what dlls are in a certain directory and I can link them somehow. One of it has the name 'pendulum.dll' and from that I gess that it implements a class type with the same name. So I have a string with the value "pendulum". How do I transform this in a codeline like:
'pendulum sys = new pendulum()'?
Define a common interface that all your simulators need to implement.
Put this interface in a separate project.
The main project has to rerference the interface project.
Create as many simulators as you wish, they all will reference the interface project (dll) and implement the defined interface
When you want to add simulators to the main project, just drop your simulator's dll and add a configuration key (or whatever) that the main application reads to know about new simulators (no recompilation required)
"Linking" at runtime can be done trought approach called loose coupling. Just create an standalone assembly with a common interface (abstract class can also used, but interface is purier), which defines the contract for physical system. Compile the simulator with reference to the assembly with the interface, and each physical system dll too.
Then you can use reflection to iterate types inside the physical system dll and look for the one which implement the interface. If you find one, instantinate it (see Activator class) and call it trought the interface.
In this approach, everything you need to no at compilet time is the interface.
where ISimulator is an interface. In my case, all actual classes are derived from an abstract class instead. I used that for typecasting, it compiled, but provoked a runtime error. After a short reflection (no pun) I got aware why this is obvious. In your posting you wrote that abstract classes can be used instead of interfaces. How?
I have a a project, where all the work is done by plugins that descend from HorusPluginBase - an abstract class that contains SOME implementation common to all plugins, but the main work is done by an abstract method that the plugin must implement. Here is how I load a plugin, using a FileSystemWatcher to look for new plugins all the time rather than at application load (this is why there are great efforts to load files into memory first; it's a nasty hack that releases the write-lock on the file so a plugin can be re-loaded without going into the complexity of creating a separate AppDomain [that can be unloaded] and marshalling across the boundary):
Code:
private void LoadPlugins()
{
StringBuilder sb = new StringBuilder().AppendLine("Horus loaded the following plugins:");
_knownPlugins.Clear(); //a list of plugins
_ancilliaryAsm.Clear();
if(_pluginWatcher == null) //the filesystemwatcher looking at the plugins dir
return;
Assembly asm = null;
string[] dllPaths = Directory.GetFiles(_pluginWatcher.Path, "*.dll", SearchOption.TopDirectoryOnly); //dir list plugins
foreach(string dllPath in dllPaths) //for each dll
{
FileInfo fi = new FileInfo(dllPath);
try {
#if DEBUG
asm = Assembly.LoadFile(fi.FullName); //load it in debug, just normally; we wont be hotswapping plugins in debug mode
#else
asm = Assembly.Load(LoadFileAsByteArray(fi.FullName)); //load into memory first, allows hotswap
#endif
foreach(Type typ in asm.GetTypes()) { //loop throu assembly looking for any relevant assignable types
if(!typ.IsAbstract && typeof(Horus.HorusPluginBase).IsAssignableFrom(typ)) {
HorusPluginBase tmPlug =
Activator.CreateInstance(typ, new object[] { _coreServices }) as HorusPluginBase;
_knownPlugins.Add(tmPlug); //add to list
sb.AppendFormat("LOAD: {0}\r\n", tmPlug.ToString());
}
}
} catch(ReflectionTypeLoadException ex) {
foreach(Exception le in ex.LoaderExceptions)
sb.AppendFormat("FAIL: {0} ({1})\r\n", fi.FullName, le.Message);
} catch(Exception ex) {
sb.AppendFormat("FAIL: {0} {2} ({1})\r\n", fi.FullName, ex.Message, ex.GetType().ToString());
}
}
sb.AppendLine("\r\n<end of plugins list>");
_coreServices.SimpleEventLog(sb.ToString());
if(_resEvtHnd == null){
_resEvtHnd = new ResolveEventHandler(CurrentDomain_AssemblyResolve);
AppDomain.CurrentDomain.AssemblyResolve += _resEvtHnd;
}
if(PluginsReloaded != null) //signal an event raise tha thte plugins were reloaded. debug windows listen to this
PluginsReloaded.Invoke(_knownPlugins);
}
//i cannot for the life of me remember exactly why I wrote this - but i *THINK* it's because
//my app bundles DLLs with it for e.g. CSV reading and the plugins can make use of them, but
//without this, they were constantly looking for the CSV reader DLLs to be alongside the
//plugin DLLs. This AssemblyResolve handler looks for the relevant assembly in the program
//folder, not the plugins folder. You might not need such functionality
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
string asmName;
string asmVer;
string asmNameVer;
try {
string[] asmBits = args.Name.Split(',');
asmName = asmBits[0];
if(asmBits.Length > 1) {
asmVer = asmBits[1];
asmBits = asmVer.Split('=');
if(asmBits.Length > 1)
asmVer = asmBits[1].Trim();
else
asmVer = "NoVersion";
} else
asmVer = "NoVersion";
asmNameVer = asmName + asmVer;
} catch(Exception ex) {
_coreServices.MultiLog(
"setupErrors",
"resolveAsmArgs",
string.Format("Attempting to resolve a reference to {0} failed: {1}",
args.Name, ex.Message
),
MultiLogType.Warn,
MultiLogMethod.All
);
return null;
}
//have we loaded this before?
if(_ancilliaryAsm.ContainsKey(asmNameVer))
return _ancilliaryAsm[asmNameVer];
//attempt to get the assembly from the plugins folder
if(_pluginWatcher == null || _pluginWatcher.Path == null)
return null;
FileInfo fInf = null;
try
{
List<FileInfo> toCheck = new List<FileInfo>();
toCheck.AddRange(new DirectoryInfo(_pluginWatcher.Path).GetFiles(asmName + ".*"));
toCheck.AddRange(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.GetFiles(asmName + ".*"));
foreach(FileInfo fi in toCheck){
fInf = fi;
string fileNameNoExt = System.IO.Path.GetFileNameWithoutExtension(fi.Name);
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(fi.FullName);
if(fvi.ProductVersion == null || fvi.FileVersion == null) {
continue;
}
string fileFileVer = fvi.FileVersion.Substring(0, fvi.FileVersion.LastIndexOf('.'));
string fileProdVer = fvi.ProductVersion.Substring(0, fvi.ProductVersion.LastIndexOf('.'));
if(asmName.Equals(fileNameNoExt) &&
(asmVer.StartsWith(fileFileVer.Replace(".0", ".")) || asmVer.StartsWith(fileProdVer.Replace(".0", ".")))
) {
#if DEBUG
_ancilliaryAsm[asmNameVer] = Assembly.LoadFile(fi.FullName);
#else
_ancilliaryAsm[asmNameVer] = Assembly.Load(LoadFileAsByteArray(fi.FullName));
#endif
return _ancilliaryAsm[asmNameVer];
} else {
//report error
string.Format("While attempting to resolve a reference to {0}, the file " +
"{1} was ignored because neither FileVersion ({2}) nor ProductVersion ({3}) matched",
args.Name, fi.FullName, fvi.FileVersion, fvi.ProductVersion
}
}
}
catch (Exception ex)
{
//report error
}
return null;
}
static byte[] LoadFileAsByteArray(string filename)
{
byte[] buffer = null;
//try 5 times with 1 second pause, to read the file - solves most File In Use problems
for(int attempts = 0; attempts < 5; attempts++) {
System.Threading.Thread.Sleep(attempts * 1000);
try {
FileStream fs = new FileStream(filename, FileMode.Open);
buffer = new byte[(int)fs.Length];
fs.Read(buffer, 0, buffer.Length);
fs.Close();
//exit the loop
attempts = 6;
} catch(IOException ioe) {
//if the file is in use, catch and loop round to read again
if(!ioe.Message.Contains("being used by another process"))
throw ioe;
}
}
return buffer;
}
note the loop i have over the assembly, to see if the type within is a subclass of my abstract class; my assembiles contain many types and they arent all subclasses of the base.. if i didnt have this check, it would go boom when i try to cast e.g. a DataTable into a HorusPluginBase
I couldnt open boudino's project to se eif he makes a similar check - i'm stuck on vs 2005 for now
What you want is a plugin framework, personally i'd recommend Mono/Addins (http://www.mono-project.com/Mono.Addins) and implement each of your simulators as an 'addin'.
NOTE: My code snippets are just snippets. They demonstrate an idea which can be adapted by you to solve your problem. They are not 100% complete and fully functional solutions equipped with error handling.
A good explaination. However this approach will prevent the dynamically loaded assemblies from EVER being unloaded. This may not be a concern for the OP, but is something to be aware of...
TheCPUWizard is a registered trademark, all rights reserved. (If this post was helpful, please RATE it!) 2008, 2009,2010 In theory, there is no difference between theory and practice; in practice there is.
* Join the fight, refuse to respond to posts that contain code outside of [code] ... [/code] tags. See here for instructions
* How NOT to post a question here
* Of course you read this carefully before you posted
* Need homework help? Read this first
A good explaination. However this approach will prevent the dynamically loaded assemblies from EVER being unloaded. This may not be a concern for the OP, but is something to be aware of...
Good point , but AFAIK, you can't unload the loaded assembly without creating new app domain, execute the code within it, Unload it. This can cause a small performance hit.
Anyway, one should select the best method based on usage scenarios, and optimizations needed.
hreba, please read the exception details to know the source and destination types.
Are you sure that you use the same assembly versions for both application and plugins ?
* The Best Reasons to Target Windows 8
Learn some of the best reasons why you should seriously consider bringing your Android mobile development expertise to bear on the Windows 8 platform.