Можно ли «украсть» обработчик событий из одного элемента управления и передать его другому? - PullRequest
12 голосов
/ 15 ноября 2008

Я хочу сделать что-то вроде этого:

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

Где btn1_Click уже определен в классе:

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

Конечно, это не скомпилируется («Событие 'System.Windows.Forms.Control.Click' может появляться только в левой части + = или - ="). Есть ли способ взять обработчик событий из одного элемента управления и назначить его другому во время выполнения? Если это невозможно, возможно ли дублировать обработчик событий и назначить его другому элементу управления во время выполнения?

Пара замечаний: я какое-то время гуглял из этого и не нашел способа сделать это. Большинство попыток подхода включают рефлексию, поэтому, если вы читаете мой вопрос и думаете, что ответ невероятно очевиден, попробуйте сначала скомпилировать код в Visual Studio. Или, если ответ на самом деле невероятно очевиден, пожалуйста, не стесняйтесь дать мне пощечину. Спасибо, я действительно с нетерпением жду возможности увидеть это.

Я знаю, что мог бы просто сделать это:

btn2.Click += new EventHandler(btn1_Click);

Это не то, что я ищу здесь.

Это тоже не то, что я ищу:

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

Ответы [ 4 ]

19 голосов
/ 15 ноября 2008

Да, это технически возможно. Отражение требуется, потому что многие из членов являются частными и внутренними. Запустите новый проект Windows Forms и добавьте две кнопки. Тогда:

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

Если это убедит вас в том, что Microsoft действительно очень старалась помешать вам сделать это, вы поняли код.

3 голосов
/ 15 ноября 2008

Нет, вы не можете этого сделать. Причина заключается в инкапсуляции - события просто подписываются / отписываются, то есть они не позволяют вам «заглянуть внутрь», чтобы увидеть, какие обработчики уже подписаны.

То, что вы могли бы сделать, это наследовать от Button и создавать публичный метод, который вызывает OnClick. Тогда вам просто нужно сделать btn1 экземпляром этого класса и подписать обработчик на btn2, который вызывает btn1.RaiseClickEvent() или любой другой метод, который вы вызываете.

Я не уверен, что я действительно рекомендую это все же. Что ты на самом деле пытаешься сделать? Какая картина больше?

РЕДАКТИРОВАТЬ: я вижу, что вы приняли версию, которая выбирает текущий набор событий с отражением, но в случае, если вы заинтересованы в альтернативе, которая вызывает обработчик OnXXX в исходном элементе управления, у меня есть образец здесь , Первоначально я скопировал все события, но это действительно приводит к очень странным эффектам. Обратите внимание, что эта версия означает, что если кто-либо подписывается на событие в исходной кнопке после вызова CopyEvents, оно все еще «подключено» - т.е. не имеет значения, когда вы связываете эти две.

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 });
        }
    }
}
1 голос
/ 16 ноября 2008

Я немного покопался с решением @ nobugz и придумал эту универсальную версию, которую можно использовать на большинстве объектов общего назначения.

Что я обнаружил, так это то, что события для, * я осмелюсь сказать, автоматические события на самом деле компилируются с полем делегата поддержки с тем же именем:

Итак, вот один для кражи обработчиков событий для более простых объектов:

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

Думал, что это может быть полезно для некоторых.

Но учтите, что способы передачи событий в компонентах Windows Forms несколько иные. Они оптимизированы так, что несколько событий не занимают много памяти, просто удерживая нули. Так что нужно еще немного покопаться, но @nobugz уже сделал это: -)

Статья Делегаты и события о объединенных делегатах может помочь прояснить множество моментов в ответах.

0 голосов
/ 15 ноября 2008

Вы можете использовать общий обработчик событий для ваших кнопок и графических блоков (согласно комментариям к предыдущему ответу), а затем использовать объект 'sender', чтобы определить, как обрабатывать событие во время выполнения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...