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()'?
hspc
June 16th, 2008, 02:03 AM
Hello hreba,
this is a simple way to do it:
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)
Instanciate simulators using Activator.CreateInstance overload (http://msdn.microsoft.com/en-us/library/d133hta4.aspx) that takes assembly(dll) name and type name.
On the returned Object handle call the Unwrap method to return an object.
Cast the returned object to the interface and use it.
This may seem complex, but it's smart and will make your appliction organized and clear.
Hope this helps.
boudino
June 16th, 2008, 02:18 AM
"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.
Do you see the point?
hreba
June 16th, 2008, 07:51 PM
Your answers seem very complete, thanks a lot. It will take me some time (during those workdays) to get through it.
boudino
June 17th, 2008, 02:53 AM
I have created a sample for you. It is done very roughtly and there is many to improve, so take it just as inspiration, not as solution.
hreba
June 17th, 2008, 06:47 PM
Thanks a lot. It is exactly what I need.
hreba
June 24th, 2008, 07:33 PM
Hi boudino,
your example code contains the line
ISimulator sim = (ISimulator)Activator.CreateInstance(t);
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?
hspc
June 25th, 2008, 01:46 AM
What was the runtime error? Type cast exception?
cjard
June 25th, 2008, 03:40 AM
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):
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());
}
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
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;
}
cjard
June 25th, 2008, 03:45 AM
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
Mutant_Fruit
June 25th, 2008, 12:37 PM
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'.
TheCPUWizard
June 25th, 2008, 02:19 PM
Hello hreba,
this is a simple way to do it:....
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...
hspc
June 25th, 2008, 05:06 PM
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 :thumb:, 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
June 25th, 2008, 07:22 PM
hspc, the error message is
"System.InvalidCastException: Cannot cast from source type to destination type."
cjard, my code is similar (Base.PhySys is my abstract class):
Base.PhySys sys;
foreach (Type t in types)
if (t.IsSubclassOf(typeof(Base.PhySys)))
{ sys = (Base.PhySys) Activator.CreateInstance(t);
break;
}
I tried it with your if-clause, with "as" instead of (<type>), nothing helps, I get the Exception above.
Mutant_Fruit, I'll have a look at these addins, I'm using mono anyway.
hspc
June 26th, 2008, 02:19 AM
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 ?
hreba
June 27th, 2008, 08:50 PM
Excuse me, the error message referred to another type cast, a few lines above.
Everything works now, thank you all for your help.
codeguru.com
Copyright Internet.com Inc., All Rights Reserved.