SourceForge.net Logo

What is ObjectPeepHole?

Daisuke Okajima

You can do following actions by the ObjectPeepHole utility for all .NET objects

You are just required to write an interface in predefined manner. ObjectPeepHole generates the implementation of the interface automatically and dynamically.

When to use?

We expect ObjectPeepHole helps you to write the code of unit tests such as NUnit.

Maybe you often modify private/protected methods or fields into public/internal by the reason that the test cases need to access them. ObjectPeepHole eliminates such turnarounds since it allows you to access private/protected member directly.

Additionally, the invocation of events is more important. For example, if you test GUI applications, the lack of ability to raise arbitrary mouse or keyboard events is large disadvantage. But .NET Framework cannot do such behavior. (As far as we know the Microsoft's .NET Framework cannot. )

ObjectPeepHole allows you to raise events only by declaring a method with a name in predefined manner. Few events are supported by methods such as Button.PerformClick(), in the other hand, ObjectPeepHole covers almost controls and events in WinForms.

We know that you can access private/protected fields or methods by using FieldInfo.GetValue() or MethodInfo.Invoke(). But we suppose that ObjectPeepHole is superior because you can treat the members in the same way as public members by using the interface.

Download

Version 1.0.0 Download(in SourceForge.net)

If you have questions or requests, please use the features of the project page in sourceforge.net or send a mail to okajima_at_lagarto.co.jp . ("@" is replaced by "_at_")

How to use?

You are required to write an interface for the target class.

For example, we explain how to access the private members or raise event for the following class Foo.

class Foo {
  private string _name;
  private int SomeMethod(string a) {
    ...
  }
  public EventHandler SomeEvent;
}
A sample interface IFooAccess is following against the class Foo.
public interface IFooAccess {
  string name { get; }       (1)
  int SomeMethod(string a);  (2)
  void FireSomeEvent(EventArgs args);  (3)
}
Now you can use IFooAccess at runtime by the following code:

Foo foo = (an instance you want to access)
PeepHoleBuilder bld = new PeepHoleBuilder(typeof(Foo), typeof(IFooAccess));
IFooAccess a = (IFooAccess)bld.CreateAccessor(foo);

string name = a.name; //get the private field _name of foo!
a.SomeMethod("zzz");  //call the private method SomeMethod of foo with the argument "zzz"!
a.FireSoomeEvent(new EventArgs()); //raise SomeEvent of foo!

Then you can access the private members or raise events by using the corresponding members of the IFooAccess interface! You are only required to write the interface. The class implementing IFooAccess is generated at runtime by ObjectPeepHole.

The rule of the interface

(1) Field

To access a private or protected field, declare a property in the interface. The names of the field and the property must be same except for the lead underscore ('_'), and the types must be same. If you want to modify the value of the field, declare the property of the interface with { get; set; } . The static fields are also supported.

(2) Method

To call a private or protected method, declare a method in the interface with the same name, the same return type, and the parameter types. The static methods are also supported.

(3) Event - 1

To raise an event, declare a method with following rules:

(4) Event - 2

Moreover, you can raise an event of a field of the target class. It is convenient to test dialog boxes. For example, if the target class is a dialog box and contains a button as below,

class SomeForm : Form {
  ...
  private Button _button1;
  ...
}
you can raise the Click event of the _button1 by the following method declaration.
interface SomeFormAccess {
  void FireButton1_Click(EventArgs args);
}

Here are the rules for this style of the event invocation.

Remarks

  1. To support the event invocation of the classes in WinForms, we investigated the internals of the Microsoft's Systme.Windows.Forms.dll. All of .NET 2.0, 3.0, and 3.5 are OK, but Mono should have different implementation.
  2. Following classes in WinForms are unsupported. Except for these, all classes and events are supported.
  3. As for method invocation, Generics and variable number of parameters are currently unsupported. We consider the support if you send a request. Output arguments (with ref or out) are OK.
  4. As for raising events, only the invocation of delegates attached to the event is executed. The corresponding protected method (for example, OnClick() against Click event) is not called.

Issues for geeks

Raising event

At first, it is severe restriction that the event invocation is allowed to only the class declaring the event itself ( derived classes are not allowed).

You may know EventInfo.GetRaiseMethod(). But, we found it is useless since it returns null always!

Then we investigated System.Windows.Forms.dll with ildasm to hack the mechanism of the event invocation. Although WinForms have many controls and events, there are only two patterns about the event invocation.

(1) A delegate as a field

This is a pattern that the class has a Delegate type field with the name corresponding to the event. For example, the IL code follows that add an event handler to the AfterLabelEdit of TreeView.

.method public hidebysig specialname instance void 
        add_AfterLabelEdit(class System.Windows.Forms.NodeLabelEditEventHandler 'value') cil managed
{
  IL_0000:  ldarg.0
  IL_0001:  dup
  IL_0002:  ldfld      class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
  IL_0007:  ldarg.1
  IL_0008:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_000d:  castclass  System.Windows.Forms.NodeLabelEditEventHandler
  IL_0012:  stfld      class System.Windows.Forms.NodeLabelEditEventHandler System.Windows.Forms.TreeView::onAfterLabelEdit
  IL_0017:  ret
} 

(2) A static key object for the event type

This is a pattern that the class has a static object for each type of the event. We can get the delegates of the event handler through EventHandlerList. For example, the IL code follows that add an event handler to the DoubleClick of Control.

.method public hidebysig specialname instance void 
        add_DoubleClick(class [mscorlib]System.EventHandler 'value') cil managed
{
  IL_0000:  ldarg.0
  IL_0001:  call       instance class [System]System.ComponentModel.EventHandlerList [System]System.ComponentModel.Component::get_Events()
  IL_0006:  ldsfld     object System.Windows.Forms.Control::EventDoubleClick
  IL_000b:  ldarg.1
  IL_000c:  callvirt   instance void [System]System.ComponentModel.EventHandlerList::AddHandler(object, class [mscorlib]System.Delegate)
  IL_0011:  ret
}

It seemed to be different which pattern to use in each class but events in a class uses same pattern. User-defined classes use the pattern (2). ObjectPeepHole generates an IL code that obtains the delegates in accordance with the patterns.

The event handlers can be called by invoking the corresponding protected method such as OnClick(), but the methods may have adverse effect because they contains the code other than calling the event handlers. Please use both ways according to the situation.

Generating a class dynamically

ObjectPeepHole generates a class dynamically according to the user-defined interface. The methods of the class are flexible because they are written by .NET IL, but ldfld or call instructions raise exceptions like FieldAccessException when the instructions access the private/protected members of the external class. This may be avoided by the configuration of the security for the assembly or the AppDomain, but we don't know. If you have information about this problem, please let us know.

We have no alternative but to invoke FieldInfo.GetValue or MethodInfo.Invoke from the IL code. As for Invoke, we must pack the arguments into object[] by considering the boxing issue and reference types. Oh it is messy...