tags:

views:

486

answers:

4

What I want to do is 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; // 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 any 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 VS 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.

Edit: I know I could just do this:

btn2.Click += new EventHandler(btn1_Click);

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

Edit: 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;
+1  A: 

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
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
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
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
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
I assumed Reflection would be required, but it's beyond my abilities.
MusiGenesis
(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
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
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
I will start a new religion based on you if this works.
MusiGenesis
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
You'd have to do that in a slightly different way, but it could certainly be done by hand.
Jon Skeet
This is all useful stuff, thanks. Let me know if you want the check and/or the new religion. You look like you're doing ok on rep, though.
MusiGenesis
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
A: 

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
+5  A: 

Yeah, it's technically possible. Reflection is required because many of the members are private and internal. Start a new WF 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.

Hans Passant
Awesome. How does "Nobugzism" sound as a religion?
MusiGenesis
That's actually not half as bad as I thought it was going to be.
MusiGenesis
Ah, I didn't realize until now that you were `nobugz` in a former life. I wondered where this "Hans Passant" character came from. :)
MusiGenesis
One and the same. Posted a few code snippets with bugz, had to give up on the nick.
Hans Passant
`Veryfewbugz` was taken, I presume? :)
MusiGenesis
+1  A: 

Did some digging around with @nobugz solution and come 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 anonymouse 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 :-)

This article about combined delegate might help clarify a lot of points in answers: http://www.yoda.arachsys.com/csharp/events.html

chakrit
Very cool, thanks.
MusiGenesis