2008-11-15 9 views
11

quiero hacer algo como esto:¿Es posible "robar" un controlador de eventos de un control y dárselos a otro?

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... 

Dónde btn1_Click ya está definido en la clase:

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

Esto no se compilará, por supuesto (System.Windows.Forms "The Event' .Control.Click 'solo puede aparecer en el lado izquierdo de + = o - = "). ¿Hay alguna manera de tomar el controlador de eventos de un control y asignarlo a otro en tiempo de ejecución? Si eso no es posible, ¿es posible duplicar el controlador de eventos y asignarlo a otro control durante el tiempo de ejecución?

Un par de puntos: he buscado este nombre en Google por un tiempo y no he encontrado manera de hacerlo todavía. La mayoría de los enfoques intentados implican reflexión, por lo que si lee mi pregunta y cree que la respuesta es increíblemente obvia, primero intente compilar el código en Visual Studio. O si la respuesta es realmente obvia, siéntete libre de abofetearme. Gracias, estoy deseando ver si esto es posible.

sé que sólo podía hacer esto:

btn2.Click += new EventHandler(btn1_Click); 

eso no es lo que estoy buscando aquí.

Esto tampoco es lo que estoy buscando:

EventHandler handy = new EventHandler(btn1_Click); 
Button btn1 = new Button(); 
btn1.Click += handy; 
Button btn2 = new Button(); 
btn2.Click += handy; 
+0

EventHandler btnClick = new EventHandler (btn1_Click); btn1.Click - = btnClick; btn2.Click + = btnClick; Si eso no ayuda, no sé si entiendo lo que quiere. – shahkalpesh

+1

Tenga en cuenta que no necesita "nuevo EventHandler (...)" a partir de C# 2 - solo "EventHandler btnClick = btn1_Click" hará ... es muy raro que necesite "nuevo WhateverDelegateType (...)" a partir de C# 2. –

+0

Lo siento, uso mucho los fragmentos nuevos para poder TAB TAB. – MusiGenesis

Respuesta

19

Sí, es técnicamente posible. Se requiere reflexión porque muchos de los miembros son privados e internos. Comience un nuevo proyecto Windows Forms y agregue dos botones.Luego:

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"); 
    } 
    } 
} 

Si esto le convence de que Microsoft intentó realmente evitar que usted haga esto, usted entendió el código.

+0

Al llamar typeof (Control) .GetField ("EventClick"), está pasando "EventClick" para obtener el campo llamado "EventClick", ¿qué cadena se debe pasar para el evento ParentChanged de Control/Button? – Brij

+0

Lo tengo "EventParent", antes probé "EventParentChanged" got excepción nula. Corrió por todos los campos obtuvo "EventParent", gracias. – Brij

2

No, no se puede hacer esto. El motivo es la encapsulación: los eventos son solo suscribirse/cancelar suscripción, es decir, no permiten "echar un vistazo dentro" para ver qué manejadores ya están suscritos.

Lo que podría hacer es derivar de Button, y crear un método público que llame al OnClick. Entonces solo necesita hacer btn1 una instancia de esa clase, y suscribir un controlador al btn2 que llama al btn1.RaiseClickEvent() o lo que sea que llame el método.

No estoy seguro de que realmente lo recomiende. ¿Qué estás tratando de hacer? ¿Cuál es la imagen más grande?

EDIT: Veo que ha aceptado la versión que capta el conjunto actual de eventos con reflejo, pero en caso de que esté interesado en la alternativa que llama al controlador OnXXX en el control original, tengo una muestra aquí. Originalmente copié todos los eventos, pero eso conduce a algunos efectos muy extraños. Tenga en cuenta que esta versión significa que si alguien se suscribe a un evento en el botón original después de llamando a CopyEvents, todavía está "conectado", es decir, no importa cuando se asocian los dos.

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 }); 
     } 
    } 
} 
+0

Quiero tomar un formulario en tiempo de ejecución, repetir su colección de controles y reemplazar todos los botones con cuadros de imagen. Hacer esto es fácil, pero luego sus cajas de imágenes de reemplazo no tienen ninguno de los eventos asociados con los botones originales. – MusiGenesis

+0

Y sé que no hay una buena razón para hacer esto. Algunas veces, como programador, puede verse limitado por fuerzas externas hasta el punto en que el loco se convierte en la salida más fácil. – MusiGenesis

+0

No creo que puedas hacer esto sin reflexión. Básicamente, estás tratando de romper la encapsulación. Puede ser por una buena razón, pero es una rotura, no obstante. Sin embargo, en lugar de buscar los controladores por reflexión, puede llamar a Button.OnClick con reflexión. –

0

Se puede usar un controlador de eventos común para sus botones y sus cuadros de imagen (según los comentarios en una respuesta anterior) y luego utilizar el objeto 'emisor' para determinar cómo manejar el evento en tiempo de ejecución.

1

Investigué un poco con la solución de @ nobugz y se me ocurrió esta versión genérica que podría usarse en la mayoría de los objetos de uso general.

Lo que descubrí es que los eventos para, me atrevería a decir, eventos automáticos en realidad están compilados con un campo respaldo delegado del mismo nombre:

Así que aquí está uno para robar controladores de eventos para los objetos más simples :

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()); } 
} 

Pensé que podría ser útil para algunos.

Pero tenga en cuenta que la forma en que los eventos están conectados en los componentes de Windows Forms es bastante diferente. Están optimizados para que los eventos múltiples no ocupen mucha memoria solo conteniendo nulos. Por lo que necesitará un poco más de excavar alrededor, pero @nobugz ya ha hecho eso :-)

El artículo Delegates and events acerca delegados combinados podrían ayudar a aclarar muchos puntos en las respuestas.

+0

Muy bueno, gracias. – MusiGenesis

Cuestiones relacionadas