15

I want do something like this:

Button btn1 = new Button();
btn1.Click += new EventHandler(btn1_Click);
Button btn2 = new Button();
// Take whatever event got assigned to btn1 and assign it to btn2.
btn2.Click += btn1.Click; // The compiler says no...

Where btn1_Click is already defined in the class:

void btn1_Click(object sender, EventArgs e)
{
    //
}

This won't compile, of course ("The event 'System.Windows.Forms.Control.Click' can only appear on the left hand side of += or -="). Is there a way to take the event handler from one control and assign it to another at runtime? If that's not possible, is duplicating the event handler and assigning it to another control at runtime doable?

A couple of points: I have googled the heck out of this one for awhile and found no way of doing it yet. Most of the attempted approaches involve reflection, so if you read my question and think the answer is incredibly obvious, please try to compile the code in Visual Studio first. Or if the answer really is incredibly obvious, please feel free to slap me with it. Thanks, I'm really looking forward to seeing if this is possible.

I know I could just do this:

btn2.Click += new EventHandler(btn1_Click);

That's not what I'm looking for here.

This is also not what I'm looking for:

EventHandler handy = new EventHandler(btn1_Click);
Button btn1 = new Button();
btn1.Click += handy;
Button btn2 = new Button();
btn2.Click += handy;
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • EventHandler btnClick = new EventHandler(btn1_Click); btn1.Click -= btnClick; btn2.Click += btnClick; If that doesn't help, I dont know if I understand what you want. – shahkalpesh Nov 15 '08 at 20:25
  • 1
    Note that you don't need "new EventHandler(...)" as of C# 2 - just "EventHandler btnClick = btn1_Click" will do... it's very rare to need "new WhateverDelegateType(...)" as of C# 2. – Jon Skeet Nov 15 '08 at 20:27
  • @shahkalpesh: sorry, I wasn't clear, but in my case the EventHandler is actually assigned to btn1 in a different scope, so that it is not around any more to be assigned to btn2. See the comments in Jon's answer for more details. – MusiGenesis Nov 15 '08 at 20:52
  • This is code smell - you are trying to break the language. Have you considered building a better system for event subscription? – Gusdor Sep 12 '13 at 09:34
  • Similar on the The Code Project: *[How to copy event handlers from one control to another at runtime](http://www.codeproject.com/Articles/308536/How-to-copy-event-handlers-from-one-control-to-ano)* – Peter Mortensen Oct 05 '13 at 20:52

4 Answers4

22

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • While calling typeof(Control).GetField("EventClick"), you are passing "EventClick" to get field named "EventClick", what string should be passed for ParentChanged event of Control/Button? – Brij Oct 09 '12 at 15:42
  • Got it "EventParent", earlier I tried "EventParentChanged" got null exception. Ran through all the Fields got "EventParent", thanks. – Brij Oct 09 '12 at 15:57
  • @HansPassant how to get TextChanged event of a textbox control? I did not find any event, or some other events can not be found :( – Inside Man Sep 22 '20 at 19:59
3

No, you can't do this. The reason is encapsulation - events are just subscribe/unsubscribe, i.e. they don't let you "peek inside" to see what handlers are already subscribed.

What you could do is derive from Button, and create a public method which calls OnClick. Then you just need to make btn1 an instance of that class, and subscribe a handler to btn2 which calls btn1.RaiseClickEvent() or whatever you call the method.

I'm not sure I'd really recommend it though. What are you actually trying to do? What's the bigger picture?

EDIT: I see you've accepted the version which fetches the current set of events with reflection, but in case you're interested in the alternative which calls the OnXXX handler in the original control, I've got a sample here. I originally copied all events, but that leads to some very odd effects indeed. Note that this version means that if anyone subscribes to an event in the original button after calling CopyEvents, it's still "hooked up" - i.e. it doesn't really matter when you associate the two.

using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;

class Test
{
    static void Main()
    {

        TextBox output = new TextBox 
        { 
            Multiline = true,
            Height = 350,
            Width = 200,
            Location = new Point (5, 15)
        };
        Button original = new Button
        { 
            Text = "Original",
            Location = new Point (210, 15)
        };
        original.Click += Log(output, "Click!");
        original.MouseEnter += Log(output, "MouseEnter");
        original.MouseLeave += Log(output, "MouseLeave");

        Button copyCat = new Button
        {
            Text = "CopyCat",
            Location = new Point (210, 50)
        };

        CopyEvents(original, copyCat, "Click", "MouseEnter", "MouseLeave");

        Form form = new Form 
        { 
            Width = 400, 
            Height = 420,
            Controls = { output, original, copyCat }
        };

        Application.Run(form);
    }

    private static void CopyEvents(object source, object target, params string[] events)
    {
        Type sourceType = source.GetType();
        Type targetType = target.GetType();
        MethodInfo invoker = typeof(MethodAndSource).GetMethod("Invoke");
        foreach (String eventName in events)
        {
            EventInfo sourceEvent = sourceType.GetEvent(eventName);
            if (sourceEvent == null)
            {
                Console.WriteLine("Can't find {0}.{1}", sourceType.Name, eventName);
                continue;
            }

            // Note: we currently assume that all events are compatible with
            // EventHandler. This method could do with more error checks...

            MethodInfo raiseMethod = sourceType.GetMethod("On"+sourceEvent.Name, 
                                                          BindingFlags.Instance | 
                                                          BindingFlags.Public | 
                                                          BindingFlags.NonPublic);
            if (raiseMethod == null)
            {
                Console.WriteLine("Can't find {0}.On{1}", sourceType.Name, sourceEvent.Name);
                continue;
            }
            EventInfo targetEvent = targetType.GetEvent(sourceEvent.Name);
            if (targetEvent == null)
            {
                Console.WriteLine("Can't find {0}.{1}", targetType.Name, sourceEvent.Name);
                continue;
            }
            MethodAndSource methodAndSource = new MethodAndSource(raiseMethod, source);
            Delegate handler = Delegate.CreateDelegate(sourceEvent.EventHandlerType,
                                                       methodAndSource,
                                                       invoker);

            targetEvent.AddEventHandler(target, handler);
        }
    }

    private static EventHandler Log(TextBox output, string text)
    {
        return (sender, args) => output.Text += text + "\r\n";
    }

    private class MethodAndSource
    {
        private readonly MethodInfo method;
        private readonly object source;

        internal MethodAndSource(MethodInfo method, object source)
        {
            this.method = method;
            this.source = source;
        }

        public void Invoke(object sender, EventArgs args)
        {
            method.Invoke(source, new object[] { args });
        }
    }
}
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • I want to take a form at runtime, iterate through its controls collection and replace all the buttons with picture boxes. Doing this is easy, but then your replacement picture boxes don't have any of the events that were associated with the original buttons. – MusiGenesis Nov 15 '08 at 20:30
  • And I do know there's no sane reason to do this. Sometimes as a programmer you can become constrained by external forces to the point where the insane becomes the easiest way out. – MusiGenesis Nov 15 '08 at 20:31
  • I don't think you'll be able to do this without reflection. Basically you're trying to break encapsulation. It may be for a good reason, but it's a breakage nonetheless. Rather than fetch the handlers by reflection though, you could call Button.OnClick with reflection. – Jon Skeet Nov 15 '08 at 20:32
  • I could call Button.OnClick that way, but I actually don't know what to call because I don't know what all events were assigned to the button (there might be a getfocus or a textchanged or whatever). – MusiGenesis Nov 15 '08 at 20:35
  • I assumed Reflection would be required, but it's beyond my abilities. – MusiGenesis Nov 15 '08 at 20:36
  • (Using reflection doesn't always violate encapsulation, btw - but the two main uses for reflection are to violate encapsulation, or (better) to handle cases where you don't know enough compile-time information about the type to satisfy the compiler.) – Jon Skeet Nov 15 '08 at 20:36
  • Hmm. Is it *just* the Button -> PictureBox here? If so, you've got a fixed set of events, so it should be feasible to do the replacement. Mind you, depending on what the event handlers are expecting, you could get some odd effects. (e.g. if they expect to change the button's text) – Jon Skeet Nov 15 '08 at 20:38
  • I'll add some code demonstrating the reflection in a minute. It'll just be Button to Button for sanity's sake, but it should give you the general idea. – Jon Skeet Nov 15 '08 at 20:38
  • I was thinking it's rare to see buttons changing their own text, but then I realized it's not rare to see them changing their own enabled state. Is it possible to leave the buttons around (just not on the form) and hook into a state change like that? – MusiGenesis Nov 15 '08 at 20:49
  • You'd have to do that in a slightly different way, but it could certainly be done by hand. – Jon Skeet Nov 15 '08 at 21:06
  • One thing to note - if you only care about EventHandler events, quite a lot of the code above isn't needed. It's there to handle the MouseEventHandler, KeyEventHandler etc events. – Jon Skeet Nov 15 '08 at 21:35
1

I did some digging around with @nobugz's solution and came up with this generic version which could be used on most general-purpose objects.

What I found out is that events for, dare I say, automatic events actually are compiled with a backing delegate field of the same name:

So here's one for stealing event handlers for simpler objects:

class Program
{
    static void Main(string[] args)
    {
        var d = new Dummy();
        var d2 = new Dummy();

        // Use anonymous methods without saving any references
        d.MyEvents += (sender, e) => { Console.WriteLine("One!"); };
        d.MyEvents += (sender, e) => { Console.WriteLine("Two!"); };

        // Find the backing field and get its value
        var theType = d.GetType();
        var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;

        var backingField = theType.GetField("MyEvents", bindingFlags);
        var backingDelegate = backingField.GetValue(d) as Delegate;

        var handlers = backingDelegate.GetInvocationList();

        // Bind the handlers to the second instance
        foreach (var handler in handlers)
            d2.MyEvents += handler as EventHandler;

        // See if the handlers are fired
        d2.DoRaiseEvent();

        Console.ReadKey();
    }
}

class Dummy
{
    public event EventHandler MyEvents;

    public void DoRaiseEvent() { MyEvents(this, new EventArgs()); }
}

Thought it might be useful to some.

But do note that the way events are wired in Windows Forms components is rather different. They are optimized so that multiple events doesn't take up a lot of memory just holding nulls. So it'll need a little more digging around, but @nobugz has already done that :-)

The article Delegates and events about combined delegates might help clarify a lot of points in answers.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
chakrit
  • 61,017
  • 25
  • 133
  • 162
0

You could use a common event handler for your buttons and your picture boxes (as per the comments on an earlier answer) and then use the 'sender' object to determine how to handle the event at runtime.

Andrew
  • 11,894
  • 12
  • 69
  • 85