Introduction
In this article I’m going to get into a .NET DLL file and extract its types, show their information, extract the members of each type, and even invoke them!!!
Yes, a useful and complete Resource Hacker. All of these are done by making use of the System.Reflection
namespace. To better understand this application, I suggest that you get
yourself familiarized with the concept of Reflection and the System.Reflection
namespace itself. The Microsoft MSDN would be a nice resource. There is also a
complete Windows Forms application for the sake of understanding this better. Enough with all this and let’s get our hands dirty.
Please note that these objects are declared globally within the main form as we will be using them a lot:
string fileName = string.Empty;
Assembly assem;
Type selectedType=typeof(Int32);
object selectedTypeInstance=null;
Letting the user select a DLL file
We have got to let the user select the DLL file they want to, so I have written the following lines of code:
private void selectDllToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = ".Net Dlls|*.dll";
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
fileName = dialog.FileName;
PopulateListBoxes();
}
}
Getting the members of the selected DLL file
The last line of code, there is a call to a method named PopulateListBoxes
which populates the listboxes we have on the form. Here it is:
private void PopulateListBoxes()
{
Reset();
assem = Assembly.LoadFile(fileName);
if (assem.GetTypes().Count() > 0)
{
btnGetInfo.Enabled = true;
}
else
{
btnGetInfo.Enabled = false ;
}
foreach (Type oType in assem.GetTypes())
{
cmbType.Items.Add(oType);
MemberInfo[] members = oType.GetMembers();
foreach (MemberInfo member in members)
{
switch (member.MemberType)
{
case MemberTypes.Constructor:
lstConstructor.Items.Add(member.Name);
break;
case MemberTypes.Event:
lstEvents.Items.Add(member.Name);
break;
case MemberTypes.Method:
lstMethods.Items.Add(member.Name);
break;
case MemberTypes.Property:
lstProperties.Items.Add(member.Name);
break;
default:
lstOthers.Items.Add(member.Name);
break;
}
}
}
cmbType.SelectedIndex = 0;
}
These ListBox
es contain the members of each type within the selected DLL file. One ListBox
for methods, one for properties, one for
events, one for constructors, and finally one for everything else. There is also a dropdownlist which has the types within the selected DLL file in
case there is more than one type in it. The Reset
method is nothing special. It just clears the listboxes and the dropdownlist, it actually resets the
application interface to its initial state.
In this PopulateListBoxes
method, I have loaded the selected DLL file into an Assembly
object using the LoadFile
method which receives a file
name as the argument: that is the file name that the Open File dialog returns. Then I have iterated through the types within the Assembly
object and added
them to the dropdownlist (there may be more than a type within a single assembly), and for each type, I have collected information about its members
by using an array of MemberInfo
. Then, I have added each item within that array to the appropriate ListBox
.
In the SelectedIndexChanged
event of the dropdownlist, I have written the following code which I think doesn’t require any explanation
as it’s almost the same as the code for the PopulateListBoxes
method. The reason behind this code is to update the listboxes when the user selects a
different type within the assembly. (All of the types within the assembly were added to the cmbType
dropdownlist already.)
lstConstructor.Items.Clear ();
lstEvents.Items.Clear ();
lstMethods.Items.Clear ();
lstProperties.Items.Clear ();
Type selectedType = assem.GetType ( cmbType.Text );
MemberInfo[] members = selectedType.GetMembers ();
foreach ( MemberInfo member in members )
{
switch ( member.MemberType )
{
case MemberTypes.Constructor:
lstConstructor.Items.Add ( member.Name );
break;
case MemberTypes.Event:
lstEvents.Items.Add ( member.Name );
break;
case MemberTypes.Method:
lstMethods.Items.Add ( member.Name );
break;
case MemberTypes.Property:
lstProperties.Items.Add ( member.Name );
break;
default:
lstOthers.Items.Add ( member.Name );
break;
}
}
Creating an object of the selected type
The code for creating a new object of the selected type is as follows. This method actually makes a call to the selected constructor
within the lstConstructors
listbox. If the selected constructor is a parameterless one, it simply creates an object. However, if the selected
constructor requires any arguments, it prompts the user to enter the arguments one by one:
selectedType = assem.GetType ( cmbType.Text );
List<Type> constructorArgs =new List<Type>();
List<object> passingArgs = new List<object> ();
ConstructorInfo[] ctors = selectedType.GetConstructors ();
foreach ( ParameterInfo param in ctors[lstConstructor.SelectedIndex].GetParameters () )
{
constructorArgs.Add( param.ParameterType);
}
ConstructorInfo ctor = selectedType.GetConstructor ( constructorArgs.ToArray<Type>() );
if ( ctor.GetParameters ().Count () == 0 )
{
selectedTypeInstance = ctor.Invoke ( null );
}
else
{
InputBox inputBox;
foreach ( ParameterInfo item in ctor.GetParameters () )
{
inputBox = new InputBox ();
inputBox.Prompt.Text =
string.Format ( "Enter a value for ' {0} ' in {1}",
item.Name, item.ParameterType );
inputBox.ShowDialog ();
switch (item.ParameterType.Name)
{
case "Int32":
passingArgs.Add(Convert.ToInt32(inputBox.SelectedValue.Text));
break;
case "String":
passingArgs.Add(Convert.ToString(inputBox.SelectedValue.Text));
break;
case "Double":
passingArgs.Add(Convert.ToDouble(inputBox.SelectedValue.Text));
break;
case "Char":
passingArgs.Add(Convert.ToChar(inputBox.SelectedValue.Text));
break;
default:
passingArgs.Add((object)inputBox.SelectedValue.Text);
break;
}
}
selectedTypeInstance = ctor.Invoke ( passingArgs.ToArray<object> () );
}
Here, inputBox
is just a Windows Form I have created myself for getting the arguments required to invoke
the method. I have made use of the ConstructorInfo
class to know the number and type of each argument that the selected
constructor requires. The Invoke
method in the last line which actually invokes the constructor requires the arguments to be an array of objects. I have created a generic
List
of object
s and in the final step, I have created it as an array of objects.
Invoking a method
The code for invoking a method of the selected type is very similar to invoking the constructor. Look at the
following lines of code which makes a call to a selected method in the lstMethods
listbox and returns the value if there is any.
object retVal = null;
List<object> passingArgs = new List<object>();
InputBox inputBox;
selectedType = assem.GetType ( cmbType.Text );
if ( selectedTypeInstance==null || selectedTypeInstance.GetType() != selectedType )
{
MessageBox.Show("No object exists of this type. Create an object first.");
return;
}
MethodInfo methodInfo = selectedType.GetMethod ( lstMethods.Text );
if ( selectedType.GetMethod ( lstMethods.Text ).GetParameters ().Count () == 0 )
{
retVal = methodInfo.Invoke ( selectedTypeInstance, null );
}
else
{
foreach ( ParameterInfo item in selectedType.GetMethod ( lstMethods.Text ).GetParameters () )
{
inputBox = new InputBox ();
inputBox.Prompt.Text = string.Format (
"Enter a value for ' {0} ' in {1}", item.Name, item.ParameterType );
inputBox.ShowDialog ();
switch (item.ParameterType.Name )
{
case "Int32":
passingArgs.Add(Convert.ToInt32( inputBox.SelectedValue.Text));
break;
case "String":
passingArgs.Add(Convert.ToString( inputBox.SelectedValue.Text));
break;
case "Double":
passingArgs.Add(Convert.ToDouble ( inputBox.SelectedValue.Text));
break;
case "Char":
passingArgs.Add(Convert.ToChar ( inputBox.SelectedValue.Text));
break;
default:
passingArgs.Add((object) inputBox.SelectedValue.Text);
break;
}
}
retVal = methodInfo.Invoke ( selectedTypeInstance, passingArgs.ToArray<object>() );
}
if ( retVal == null )
{
lblInfo.Text= "Return Value: Null" ;
}
else
{
lblInfo.Text= ( string.Format ( "Return Value: {0}", retVal.ToString () ) );
}
Here I have created a MethodInfo
object for getting the type of each argument that the method requires. If the method is parameterless,
I simply call it and return the value, but if the method requires parameters, I have to know the type of each one, so I call the GetParameters()
method of the selected method within the selected type and iterate within each one by making use of a ParameterInfo
object (a little confused here!!!).
In the iteration loop, I prompt the user to enter the value for each parameter one by one. And finally I call the Invoke
method on the selected method and pass
the gathered arguments and get the returned value.
Setting and getting the values of a property
For setting the values of properties and getting values back, we have two options. We can either call the setter and getter methods of each property that is listed
in the lstMethods
listbox or we can get and set the values directly using the properties themselves. I have used the latter
approach. The following lines of code are for setting the values of a property selected in the lstProperties
listbox:
selectedType = assem.GetType(cmbType.Text);
if (selectedTypeInstance == null || selectedTypeInstance.GetType() != selectedType)
{
MessageBox.Show("No object exists of this type. Create an object first.");
return;
}
if (! selectedType.GetProperty ( lstProperties.Text ).CanWrite )
{
lblInfo.Text= "This property is Read-Only." ;
return;
}
InputBox inputBox = new InputBox ();
inputBox.Prompt.Text = string.Format ( "Enter a value for ' {0} ' in {1}",
lstProperties.Text, selectedType.GetProperty ( lstProperties.Text ).PropertyType );
inputBox.ShowDialog ();
Type type = selectedTypeInstance.GetType ();
PropertyInfo prop = type.GetProperty ( lstProperties.Text );
switch (prop.PropertyType.Name )
{
case "Int32":
prop.SetValue ( selectedTypeInstance,Convert.ToInt32( inputBox.SelectedValue.Text), null );
break;
case "String":
prop.SetValue ( selectedTypeInstance,Convert.ToString( inputBox.SelectedValue.Text), null );
break;
case "Double":
prop.SetValue ( selectedTypeInstance,Convert.ToDouble( inputBox.SelectedValue.Text), null );
break;
case "Char":
prop.SetValue ( selectedTypeInstance,Convert.ToChar ( inputBox.SelectedValue.Text), null );
break;
default:
prop.SetValue ( selectedTypeInstance,(object) (inputBox.SelectedValue.Text), null );
break;
}
I have created a PropertyInfo
object to get to the selected property in lstProperties
and then depending on the type of the property, I set the gathered
value using the SetValue
method of the PropertyInfo
class. Almost all of the codes we have in this application such as invoking a method
and setting the values of the properties require an object to operate on. This object must be of the type of the class we are invoking a method from. The selectedTypeInstance
object here is the object of the selected type that we do our operations on.
The following is for getting the value back from a property, which is much simpler:
if (selectedTypeInstance == null || selectedTypeInstance.GetType() != selectedType)
{
MessageBox.Show("No object exists of this type. Create an object first.");
return;
}
if ( !selectedType.GetProperty ( lstProperties.Text ).CanRead )
{
lblInfo.Text= "This property is Write-Only." ;
return;
}
Type type = selectedTypeInstance.GetType ();
PropertyInfo prop = type.GetProperty ( lstProperties.Text );
object propertyValue= prop.GetValue ( selectedTypeInstance,null);
lblInfo.Text = string.Format ( "Property Value: {0}", propertyValue.ToString () );
In both getting and setting the values of a property, we have to make sure we have the authorization. I mean, if the property is read-only,
we cannot set its value and if it’s write-only, we cannot get its value. This is checked by the CanWrite
and CanRead
properties
of the PropertyInfo
class, respectively.
Viewing the body of a method
I have added the following code for viewing the machine code of a selected method. After getting the machine code, we can convert it to the MSIL code and then
to the original C# code of the method. Amazing, huh? I haven’t gone that far in this application, however.
StringBuilder sb = new StringBuilder ();
Type selectedType = assem.GetType ( cmbType.Text );
MethodInfo mi = selectedType.GetMethod ( lstMethods.Text );
MethodBody mb = mi.GetMethodBody ();
sb.AppendLine (string.Format( "Method: {0}", mi.Name ));
sb.AppendLine ( string.Format ( "Local variables are initialized: {0}", mb.InitLocals ) );
sb.AppendLine ( string.Format ( "Maximum number of items on the operand stack: {0}",
mb.MaxStackSize ) );
foreach ( LocalVariableInfo lvi in mb.LocalVariables )
{
sb.AppendLine(string.Format ( "Local variable: {0}", lvi ));
}
sb.AppendLine ();
sb.AppendLine ( "The Machine Code of the Method: " );
byte[] ilb = mb.GetILAsByteArray ();
for ( int i = 0 ; i < ilb.Length ; i++ )
sb.AppendLine(string.Format ( "{0:X} ", ilb[i] ));
lblInfo.Text = sb.ToString ();
Showing the information about each member
I wanted this application to be very informative. Therefore, I have put some lines of code in the SelectedIndexChanged
of the listboxes that hold the
members. When a member is selected in each listbox, some useful information about it will be shown to the user. I have put the following code
in the SelectedIndexChanged
of the lstConstructors
listbox.
if (lstConstructor.SelectedIndex == -1)
{
button1.Enabled= false;
}
else
{
button1.Enabled = true;
}
StringBuilder sb = new StringBuilder ();
Type selectedType = assem.GetType ( cmbType.Text );
ConstructorInfo[] ctors = selectedType.GetConstructors ();
foreach ( ParameterInfo param in ctors[lstConstructor.SelectedIndex].GetParameters () )
{
sb.AppendLine ( ( string.Format ( "Parameter {0} is named {1} and is of type {2}",
param.Position, param.Name, param.ParameterType ) ) );
}
if ( sb.ToString () == string.Empty )
{
lblInfo.Text = "This is a default constructor and doesn't have any parameters.";
}
else
{
lblInfo.Text = sb.ToString ();
}
For getting the information about the selected method in the lstMethods
listbox, I have put the following lines of code in the SelectedIndexChanged
event handler
of the lstMethods
listbox:
if (lstMethods.SelectedIndex == -1)
{
button6.Enabled = button2.Enabled = false;
}
else
{
button6.Enabled = button2.Enabled = true;
}
Type selectedType = assem.GetType(cmbType.Text);
MethodInfo mi = selectedType.GetMethod(lstMethods.Text);
StringBuilder sb = new StringBuilder();
sb.AppendLine(string.Format("Name: {0}", mi.Name));
sb.AppendLine(string.Format("Is Public: {0}", mi.IsPublic));
sb.AppendLine(string.Format("Is Private: {0}", mi.IsPrivate));
sb.AppendLine(string.Format("Is Static: {0}", mi.IsStatic));
sb.AppendLine(string.Format("Is Virtual: {0}", mi.IsVirtual));
sb.AppendLine(string.Format("Return Parameter: {0}", mi.ReturnParameter));
sb.AppendLine(string.Format("Return Type: {0}", mi.ReturnType));
sb.AppendLine(string.Format("Parameters:"));
foreach (ParameterInfo item in mi.GetParameters())
{
sb.AppendLine(string.Format("Name: {0}", item.Name));
sb.AppendLine(string.Format("Parameter Type: {0}", item.ParameterType));
sb.AppendLine(string.Format("Is In: {0}", item.IsIn));
sb.AppendLine(string.Format("Is Out: {0}", item.IsOut));
sb.AppendLine(string.Format("Is Optional: {0}", item.IsOptional));
sb.AppendLine(string.Format("Is Retval: {0}", item.IsRetval));
sb.AppendLine(string.Format("Default Value: {0}", item.DefaultValue));
sb.AppendLine();
}
lblInfo.Text = sb.ToString();
For getting to the information about the selected property in the lstProperties
listbox:
if (lstProperties.SelectedIndex==-1)
{
button4.Enabled = button5.Enabled = false;
}
else
{
button4.Enabled = button5.Enabled = true;
}
Type selectedType = assem.GetType(cmbType.Text);
PropertyInfo pi = selectedType.GetProperty(lstProperties.Text);
StringBuilder sb = new StringBuilder();
sb.AppendLine(string.Format("Name: {0}", pi.Name));
sb.AppendLine(string.Format("DeclaringType: {0}", pi.DeclaringType));
sb.AppendLine(string.Format("Can Read: {0}", pi.CanRead));
sb.AppendLine(string.Format("Can Write: {0}", pi.CanWrite));
sb.AppendLine(string.Format("PropertyType: {0}", pi.PropertyType));
lblInfo.Text = sb.ToString();
In the SelectedIndexChanged
event handler of the lstEvents
listbox, we have:
Type selectedType = assem.GetType(cmbType.Text);
EventInfo ei = selectedType.GetEvent(lstEvents.Text);
StringBuilder sb = new StringBuilder();
sb.AppendLine(string.Format("Name: {0}", ei.Name));
sb.AppendLine(string.Format("Is Multicast: {0}", ei.IsMulticast));
sb.AppendLine(string.Format("Is SpecialName: {0}", ei.IsSpecialName));
sb.AppendLine(string.Format("Raise Method: {0}",ei.GetRaiseMethod(true).Name ));
sb.AppendLine(string.Format("Remove Method: {0}", ei.GetRemoveMethod().Name ));
lblInfo.Text = sb.ToString();
And finally, to get some information about the selected type itself, I have the following lines of code:
Type selectedType = assem.GetType(cmbType.Text);
StringBuilder sb = new StringBuilder();
sb.AppendLine(string.Format("AssemblyQualifiedName: {0}", selectedType.AssemblyQualifiedName));
sb.AppendLine(string.Format("Namespace: {0}", selectedType.Namespace));
sb.AppendLine(string.Format("Is IsClass: {0}", selectedType.IsClass));
sb.AppendLine(string.Format("BaseType: {0}", selectedType.BaseType));
sb.AppendLine(string.Format("Is ValueType: {0}", selectedType.IsValueType));
sb.AppendLine(string.Format("Is IsSerializable: {0}", selectedType.IsSerializable));
sb.AppendLine(string.Format("Is IsSealed: {0}", selectedType.IsSealed));
sb.AppendLine(string.Format("Is IsPublic: {0}", selectedType.IsPublic));
sb.AppendLine(string.Format("Is IsNested: {0}", selectedType.IsNested));
sb.AppendLine(string.Format("Is IsInterface: {0}", selectedType.IsInterface));
sb.AppendLine(string.Format("Is IsGenericType: {0}", selectedType.IsGenericType));
sb.AppendLine(string.Format("Is IsAbstract: {0}", selectedType.IsAbstract));
lblInfo.Text=sb.ToString();
And that’s all. Hope you find it useful and practical. Till my next article, have a good time programming.