How to "Steal" an Event Handler from One Control and Give It to Another

Is it possible to steal an event handler from one control and give it to another?

Yeah, it's technically possible. Reflection is required because many of the members are private and internal. Start a new Windows Forms project and add two buttons. Then:

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Reflection;

namespace WindowsFormsApplication1 {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
button1.Click += new EventHandler(button1_Click);
// Get secret click event key
FieldInfo eventClick = typeof(Control).GetField("EventClick", BindingFlags.NonPublic | BindingFlags.Static);
object secret = eventClick.GetValue(null);
// Retrieve the click event
PropertyInfo eventsProp = typeof(Component).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
EventHandlerList events = (EventHandlerList)eventsProp.GetValue(button1, null);
Delegate click = events[secret];
// Remove it from button1, add it to button2
events.RemoveHandler(secret, click);
events = (EventHandlerList)eventsProp.GetValue(button2, null);
events.AddHandler(secret, click);
}

void button1_Click(object sender, EventArgs e) {
MessageBox.Show("Yada");
}
}
}

If this convinces you that Microsoft tried really hard to prevent your from doing this, you understood the code.

Copy all event handlers from one control to another at runtime

I finally found something that works here:

Here is the solution I'm using:

using System;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using BF = System.Reflection.BindingFlags;

namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
btn1.Click += (s, e) => Console.WriteLine($"{((Button)s).Content}a pressed");
btn1.Click += Btn1_Click;
btn1.MouseEnter += (s, e) => Console.WriteLine($"{((Button)s).Content} mouse entered");

AddButton();
}

private void Btn1_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine($"{((Button)sender).Content}b pressed");
}


private void AddButton()
{
Button btn2 = new Button() { Content = "Button 02" };
panel.Children.Add(btn2);

// Copy all event handler from btn1 to btn2 ??
FieldInfo[] fields = btn1.GetType().GetFields(BF.Static | BF.NonPublic | BF.Instance | BF.Public | BF.FlattenHierarchy);
foreach (FieldInfo field in fields.Where(x => x.FieldType == typeof(RoutedEvent)))
{
RoutedEventHandlerInfo[] routedEventHandlerInfos = GetRoutedEventHandlers(btn1, (RoutedEvent)field.GetValue(btn1));
if (routedEventHandlerInfos != null)
{
foreach (RoutedEventHandlerInfo routedEventHandlerInfo in routedEventHandlerInfos)
btn2.AddHandler((RoutedEvent)field.GetValue(btn1), routedEventHandlerInfo.Handler);
}
}
}


/// <summary>
/// Get a list of RoutedEventHandlers
/// Credit: Douglas : https://stackoverflow.com/a/12618521/3971575
/// </summary>
/// <param name="element"></param>
/// <param name="routedEvent"></param>
/// <returns></returns>
public RoutedEventHandlerInfo[] GetRoutedEventHandlers(UIElement element, RoutedEvent routedEvent)
{
// Get the EventHandlersStore instance which holds event handlers for the specified element.
// The EventHandlersStore class is declared as internal.
PropertyInfo eventHandlersStoreProperty = typeof(UIElement).GetProperty("EventHandlersStore", BF.Instance | BF.NonPublic);
object eventHandlersStore = eventHandlersStoreProperty.GetValue(element, null);

// If no event handlers are subscribed, eventHandlersStore will be null.
// Credit: https://stackoverflow.com/a/16392387/1149773
if (eventHandlersStore == null)
return null;

// Invoke the GetRoutedEventHandlers method on the EventHandlersStore instance
// for getting an array of the subscribed event handlers.
MethodInfo getRoutedEventHandlers = eventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", BF.Instance | BF.Public | BF.NonPublic);

return (RoutedEventHandlerInfo[])getRoutedEventHandlers.Invoke(eventHandlersStore, new object[] { routedEvent });
}

}
}

With this, I'm assigning both handlers for the Click event and the one for MouseEntered event to the second button.

Assign an EventHandler from a control using it

Simply: you cannot use an EventHandler of a control's event and assign it to another event. So this isn't possible:

control2.ShownEditor += control1.ShownEditor; //wrong

The only way is to create an EventHandler separately and assign it to both the controls' event.

Another harmful solution could be extract the delegate via Reflection, but as I said it's actually dangerous, look this answer by Hans Passant: Is it possible to “steal” an event handler from one control and give it to another?.

Get delegate handler from event

Events don't actually have handlers; an event is just a pair of specially-named add & remove methods.

For more information, see my blog.

How the event stores its handlers is an implementation detail; WinForms controls use an EventHandlerList.

You can see this in the source.

How to set an EventHandler equal to another EventHandler

We could copy the events via reflection. Now i myself would be wary of doing this, so please test exhaustively and with all versions (2.0, 3.0, 4.0). I tried many ways but the following was the only way, i got it to work. A Smoke test was run on .NET 4.0.

Create an extension method on the Form Class

public static class FormExtension
{
public static void CopyEvent(this Form form, Control src, string fieldName, string eventName, Control dest)
{
EventHandlerList events = (EventHandlerList)typeof(Control)
.GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(src, null);
object key = typeof(Control).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
EventInfo evInfo = typeof(Control).GetEvent(eventName, BindingFlags.Public | BindingFlags.Instance);
Delegate del = events[key];
if (del != null)
{
Delegate d = Delegate.CreateDelegate(evInfo.EventHandlerType, form, del.Method);
MethodInfo addHandler = evInfo.GetAddMethod();
Object[] addHandlerArgs = { d };
addHandler.Invoke(dest, addHandlerArgs);
}
}
}

Now use it like this

Here i show an example of copying the click and the text changed event.

this.CopyEvent(richTextBox1, "EventText", "TextChanged", richTextBox2);
this.CopyEvent(richTextBox1, "EventClick", "Click", richTextBox2);

How to use it for other events

You would have to open the Control class via Reflector and get the field and the eventnames.

So in the case of Text Changed it was something like:

public event EventHandler TextChanged    <-----The Event name for the "CopyEvent" function
{
add
{
base.Events.AddHandler(EventText, value);
}
remove
{
base.Events.RemoveHandler(EventText, value);
}
}

where EventText is

private static readonly object EventText = new object();  <-------The Field name

How to pass an event to a method and then subscribe to it?

You are mixing events with delegates. You should probably read this article by Jon Skeet which will explain the differences between the two.

The reason the second option doesn't work is because the method expects a parameter that is a delegate that conforms to the EventHandler signature.Button.Click refers to an event rather than a delegate. Events just encapsulate delegates.

I'm not sure what you are trying can be done. Conceptually what you want to do doesn't actually make sense; it is the logic of the handler you want to pass around not the event itself.

Take a look at this post which looks at some ways of simulating the effect you want.



Related Topics



Leave a reply



Submit