Introduction
This code allows you to get a delegate that subscribed to a Control's event. The technique used is applicable to events in general.
Background
Recently I came across a problem. The task was to extend the functionality of some third-party control. No source code is available, all public
classes are sealed. I needed to change the behavior of a button that showed SaveFileDialog
and then rendered a report with default settings. There was no way to change the settings of render but to rewrite all control's toolbar.
But all I needed was to change the handler of a Click
event on a control's button. To do that, one should unsubscribe old handler (-=) and subscribe a new one (+=). The question is - how to get the old handler when you don't have access to it?
To think about it, the same situation arises when a developer wants to unsubscribe from an anonymous delegate or lambda expression. There is no name to reference them (apart from storing it beforehand in a variable. But anonymous delegates, you know... should stay anonymous).
Research
First let's see the event storing background. I was to modify the behavior of a ToolStripButton
so we'll dig from it.
Reflector shows that there are only two events in the ToolStripButton
class. But there are a bunch of them in the base ToolStripItem
class. The Click
event for example is defined like:
public event EventHandler Click
{
add { base.Events.AddHandler(EventClick, value); }
remove { base.Events.RemoveHandler(EventClick, value); }
}
We see that there is an Events collection that stores all subscribed delegates. And the kind of event is determined by the key object - EventClick
in this case:
public abstract class ToolStripItem: Component, ...
{
...
internal static readonly object EventClick;
...
}
The Events collection (of type EventHandlerList
) is stored in System.ComponentModel.Component
class and has an interesting method Find
:
public sealed class EventHandlerList : ...
{
...
private ListEntry Find(object key);
...
private sealed class ListEntry
{
...
internal Delegate handler;
...
}
}
Implementation
In summary, to get access to a delegate that subscribed to the Click
event, one could write something like:
control.Events.Find( control.EventClick ).handler
But encapsulation won't allow it. Luckily there is a way to achieve that - reflection. The code will be:
var handler = (EventHandler) GetDelegate( toolStripButton, "EventClick" );
private static object GetDelegate( Component issuer, string keyName )
{
var key = issuer
.GetType( )
.GetField( keyName, BindingFlags.Static |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy )
.GetValue( null );
var events = typeof( Component )
.GetField( "events", BindingFlags.Instance | BindingFlags.NonPublic )
.GetValue( issuer );
var listEntry = typeof( EventHandlerList )
.GetMethod( "Find", BindingFlags.NonPublic | BindingFlags.Instance )
.Invoke( events, new object[] { key } );
var handler = listEntry
.GetType( )
.GetField( "handler", BindingFlags.Instance | BindingFlags.NonPublic )
.GetValue( listEntry );
return handler;
}
Available event keys (it's not only "EventClick
" you know...) can be accessed with a method:
private static IEnumerable<string> GetEventKeysList( Component issuer )
{
return
from key in issuer.GetType( ).GetFields( BindingFlags.Static |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy )
where key.Name.StartsWith( "Event" )
select key.Name;
}
The code with the article contains a project that demonstrates all techniques described.
Conclusion
The event manipulation syntax in C# was made restrictive with a purpose. It will not break event encapsulation. It is by design and it is the right thing.
A situation when you need to access an unknown delegate that subscribed to a known event is rare. But it exists. I hope this article saved you some time when you came across such a problem.
History
- 5th April, 2009: Initial post