Oct 13, 2014

Using reflection to load unreferenced assemblies at runtime

Sure, we can define our application's referenced assemblies easily, but how would one interact with types in assemblies at runtime that we don't directly reference? This can be quite a problem for someone who is creating plugin or addon support for their application, where the assemblies they must reference will be added post-build. One of the functions of the System.Reflection namespace is loading assemblies and accessing their contained types.


Wikipedia said:
System.Reflection
Provides an object view of types, methods, and fields. You have "the ability to dynamically create and invoke types". It exposes the API to access the Reflective programming capabilities of CLR.


Reflecting on an assembly and its types

To demonstrate this, let's setup a hypothetical situation in which we have an application, and a .NET DLL unreferenced by our application whose types we need to access at runtime. The DLL will be called MyDLL.dll, and the application will be called MyApp.exe. How would we view the types contained inside MyDLL from MyApp at runtime when MyDLL isn't referenced by MyApp? The System.Runtime.Assembly class can be used to access MyDLL in the following manner
1System.Reflection.Assembly myDllAssembly = System.Reflection.Assembly.LoadFile("%MyDLLPath%\\MyDLL.dll");

The myDllAssembly object can now be used to access the types contained inside MyDLL. To access a type contained inside MyDLL, System.Reflection.Assembly possesses a method called GetType() which can return a System.Type that can be used to access all the members of the type we want to retrieve from inside MyDLL. Note that the System.Reflection.Assembly.GetType() method is not to be confused with the GetType() method possessed by all objects; they are related, but quite different and we will discuss the other GetType() method later. Back to our example, let's say there is a type inside MyDLL called MyDLLForm which inherits from System.Windows.Forms.Form and is contained in the namespace MyDLLNamespace. To obtain the System.Type of this, we could do the following
1System.Reflection.Assembly myDllAssembly = System.Reflection.Assembly.LoadFile("%MyDLLPath%\\MyDLL.dll");
2System.Type MyDLLFormType = myDllAssembly.GetType("MyDLLNamespace.MyDLLForm");

MyDLLFormType now holds the System.Type of MyDLLForm. We can now access all the members and contained types inside MyDLLForm from MyDLLFormType. However, using this GetType() method will not create an instance of MyDLLForm. To access a type contained inside MyDLL and create an instance of it, System.Reflection.Assembly possesses another method called CreateInstance(). Say we wanted to create an instance of MyDLLForm, we could use something like this
1System.Reflection.Assembly myDllAssembly = System.Reflection.Assembly.LoadFile("%MyDLLPath%\\MyDLL.dll");
2Form MyDLLFormInstance = (Form)myDllAssembly.CreateInstance("MyDLLNamespace.MyDLLForm");

MyDLLFormInstance now holds an instance of MyDLLForm (remember that MyDLLForm inherits from Form). To create an instance of MyDLLForm, that method will invoke MyDLLForm's constructor. By way of that overload, CreateInstance is expecting a constructor that takes no arguments, as we only passed it the type name we want to create an instance of. However, lets say there is a constructor we want to use to create an instance of MyDLLForm that takes two arguments, a string and an int. To do that, and for example to pass it a string of "Hi" and an integer value of 113 (one of my favorite numbers), we could use this
01System.Reflection.Assembly myDllAssembly = System.Reflection.Assembly.LoadFile("%MyDLLPath%\\MyDLL.dll");
02 
03//the arguments we will pass
04object[] argstopass = new object[] { (object)"Hi", (object)113 };
05 
06Form MyDLLFormInstance = (Form)myDllAssembly.CreateInstance("MyDLLNamespace.MyDLLForm",
07     false, //do not ignore the case
08     BindingFlags.CreateInstance, //specifies we want to call a ctor method
09     null, //a null binder specifies the default binder will be used (works in most cases)
10     argstopass, //the arguments (based on the arguments, CreateInstance() will decide which ctor to invoke)
11     null, //CultureInfo is null so we will use the culture info from the current thread
12     null //no activation attributes
13     );

Read the comments, they should tell you what's happening pretty good.

Reflecting on a single type


Fields

The CreateInstance() method can easily create an instanced object of any type in an assembly we've loaded in a System.Reflection.Assembly, however, you can see that in the example above MyDLLFormInstance is of type System.Windows.Forms.Form, which is the inherited type of MyDLLForm. However, say there are custom members contained inside MyDLLForm that we need to access. MyDLLFormInstance is of type System.Windows.Forms.Form, so any members we have created inside MyDLLForm we cannot access directly by MyDLLFormInstance. This is where that other GetType() methods that all objects possess comes in handy. Lets say for an example, there is a public string variable contained inside MyDLLForm called StringVariable. To retrieve the value of that variable from MyDLLFormInstance, the following code can be used
1string StringVariableValue = (String)MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetField("StringVariable") //Get a System.Reflection.FieldInfo object representing StringVariable
4    .GetValue(MyDLLFormInstance); //Get the actual value of StringVariable

foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
    foreach (Type t in a.GetTypes())
    {
        // ... do something with 't' ...
    }
}
That will assign StringVariableValue to the value of StringValue contained in MyDLLFormInstance. The GetType() method is used to get the System.Type of MyDLLFormInstance. It will return a type that represents MyDLLForm, because even though MyDLLFormInstance is declared as a Form, it's actual value is of MyDLLForm. We can then use the GetField() method of that System.Type to return the value of the StringVariable field in MyDLLFormInstance.

Say, however, we want to set StringVariable in that same instance instead of retrieving its value. We can use almost the same method, except instead of calling the GetValue() method of the FieldInfo that represents StringValue, we can use the SetValue() method like so
1MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetField("StringVariable") //Get a System.Reflection.FieldInfo object representing StringVariable
4    .SetValue(MyDLLFormInstance, //the object whose StringVariable field we want to set (MyDLLFormInstance)
5        (object)"This string will be the new value of StringVariable" //the new value of StringVariable
6        );

That will then set the StringVariable field in MyDLLFormInstance.

Properties

But what if there is a property in MyDLLForm that we want to retrieve in MyDLLFormInstance, called StringProperty, for example. We can use this method to get the property's value
1MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetProperty("StringProperty") //Gets a System.Reflection.PropertyInfo object representing StringProperty
4    .GetValue(MyDLLFormInstance, null); //Gets the property's value

Now, if we wanted to set the value of the same property (if the property isn't read-only)
1MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetProperty("StringProperty") //Gets a System.Reflection.PropertyInfo object representing StringProperty
4    .SetValue(MyDLLFormInstance, (object)"This will be the new value of StringProperty", null); //Sets the value of StringProperty


Methods

Now you know how to modify and retrieve the value of public fields and properties contained inside assemblies loaded in an System.Reflection.Assembly object. However, probably the most important is knowing how to invoke methods. So, using the same MyDLL scenario, lets say there is a method in MyDLLForm that returns void and takes no arguments called SomeMethod. This will be quite easy to invoke, as there is no arguments to pass, and we will not need to retrieve the value. To invoke that method, we can easily use, on the same MyDLLFormInstance object
1MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetMethod("SomeMethod") //Gets a System.Reflection.MethodInfo object representing SomeMethod
4    .Invoke(MyDLLFormInstance, null); //here we invoke SomeMethod. The arguments list is null because it takes no arguments

Lets make this situation a bit more complex. Lets say SomeMethod takes 2 arguments, a string and an int. Lets use the same two example values from our CreateInstance example; "Hi" as the string and 113 and the integer. Remember, SomeMethod takes arguments, but it still returns void, so we don't need to retrieve the value. To invoke SomeMethod this time, we could use
1//This is the arguments list. "Hi" is the string and 113 is the int
2object[] argstopass = new object[] { (object)"Hi", (object)113 };
3 
4MyDLLFormInstance
5    .GetType() //Get the type of MyDLLForm
6    .GetMethod("SomeMethod") //Gets a System.Reflection.MethodInfo object representing SomeMethod
7    .Invoke(MyDLLFormInstance, argstopass); //here we invoke SimpleMethod. We also pass ArgsToPass as the argument list

Now, let's make this as complicated as we can get it. Lets say that SomeMethod takes those same arguments, and returns a value of string which you need to retrieve when you invoke SomeMethod. This is really quite easy, because the Invoke() method we used in the past two examples returns a type of object, which will hold the value returned by the method it invoked. So, to get the string returned by SomeMethod, we can use
1//This is the arguments list. "Hi" is the string and 113 is the int
2object[] argstopass = new object[] { (object)"Hi", (object)113 };
3 
4string ReturnValue = (string)MyDLLFormInstance
5    .GetType() //Get the type of MyDLLForm
6    .GetMethod("SomeMethod") //Gets a System.Reflection.MethodInfo object representing Some
7    .Invoke(MyDLLFormInstance, argstopass) //here we invoke SomeMethod. We also pass ArgsToPass as the argument list

In that example, ReturnValue will hold the value returned by SomeMethod when we invoked it.

Events

Now, what about events? Lets say we have an event in MyDLLForm called SomeEvent that we want to access in MyDLLFormInstance. To obtain a System.Reflection.EventInfo object representing that event, simply use
1MyDLLFormInstance
2    .GetType() //Get the type of MyDLLForm
3    .GetEvent("SomeEvent"); //Gets a System.Reflection.EventInfo object representing SomeEvent

Now, lets pretend that event uses a simple EventHandler delegate. To add an event handler for that event, use
01MyDLLFormInstance
02    .GetType() //Get the type of MyDLLForm
03    .GetEvent("SomeEvent") //Gets a System.Reflection.EventInfo object representing SomeEvent
04    .AddEventHandler(MyDLLFormInstance, new EventHandler(SomeEventHandler)); //Adds SomeEventHandler as an event handler for SomeEvent
05 
06//------------------------------------------------------------------------------------------
07 
08//This method will be the event handler for SomeEvent in MyDLLFormInstance
09public void SomeEventHandler(object sender, EventArgs e)
10{
11 
12}

And to remove that same event handler
01MyDLLFormInstance
02    .GetType()
03    .GetEvent("SomeEvent") //Gets a System.Reflection.EventInfo object representing SomeEvent
04    .RemoveEventHandler(MyDLLFormInstance, new EventHandler(SomeEventHandler)); //Removes SomeEventHandler as an event handler for SomeEvent
05 
06//------------------------------------------------------------------------------------------
07 
08//This method will be removed as the event handler for SomeEvent in MyDLLFormInstance
09public void SomeEventHandler(object sender, EventArgs e)
10{
11 
12 
13}


Nested Types

What if there are types contained inside a type in an assembly? That is quite easy. Going along with our example, let's say MyDLLForm contains a nested class called MyDLLFormNestedType. To access that, use
1myDllAssembly
2    .GetType("MyDLLForm") //Gets a type representing MyDLLForm
3    .GetNestedType("MyDLLFormNestedType") //Gets a type representing MyDLLFormNestedType, which is nested inside MyDLLForm

Notice we are not accessing MyDLLFormNestedType through MyDLLFormInstance, but instead we are accessing the assembly with MyDllAssembly, and then retrieving the System.Type of MyDLLForm, and from that retrieving the System.Type of the nested type MyDLLFormNestedType. You can then interact with that nested type the same as you can interact with the type containing it (creating instances of it, accessing the members contained in it, etc).

Though this example we used throughout this tutorial was pertaining to loading a class inheriting from form contained inside a DLL, don't for a second think that i'm implying that you can only access classes contained inside an assembly. You can access any type, class, struct, interface, or anything. Not only can System.Reflection be used to load unreferenced assemblies, as demonstrated in this tutorial, but it can be used to make powerful decompilers and code analyzers. .NET Reflector, a powerful .NET decompiler, would not exist if it wasn't for the System.Reflection namespace and the reflective capabilities of the CLR.

0 comments:

Post a Comment

Nam Le © 2014 - Designed by Templateism.com, Distributed By Templatelib