C# copy one control event to another programmatically [duplicate] - c#

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;

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.

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

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.

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.

Related

Attach event handler to be called only once

I am currently trying to write an extension function to be able to easily attach an action that is only used once when the event is fired, then unsubscribed.
I am trying something like this:
public static void AttachOnce<TEventArgs>([NotNull] this EventHandler<TEventArgs> me, [NotNull] Action<object, TEventArgs> action)
where TEventArgs : System.EventArgs
{
var handler = me;
EventHandler<TEventArgs> wrappedAction = null;
wrappedAction = (sender, args) =>
{
action(sender, args);
handler -= wrappedAction;
};
handler += wrappedAction;
}
But ReSharper complains on the unsubscribe that handler is "Access to modified closure".
I know what this means, so I made the local variable for the closure already, but it doesn't seem to resolve it. What is failing here?
The direct hard-coded code works. Something like this:
var file = new FileSystemWatcher("path/to/file");
FileSystemEventHandler handler = null;
handler = (sender, args) =>
{
// My action code here
file.Changed -= handler;
};
file.Changed += handler;
EDIT 1 (2018-10-09 11:43 CET):
I may just have been too fast, asking a question before thoroughly thinking it through.
You can't create extension methods on Events. At all. It's just not possible in C#. So I can't even test why ReSharper is complaining and if it is right, because a call like file.Changed.AttachOnce(action) is not valid. It says "The event 'Changed' can only appear on the left hand side of += or -=".
I have found some more sources for similar requests/questions:
http://www.hardkoded.com/blog/csharp-wishlist-extension-for-events
One time generic event call?
I've been thinking about a different but much simpler approach, using a "self-detaching" inline handler which would be used like this:
obj.Event += (s, e) =>
{
Detach(obj, nameof(obj.Event));
// ..do stuff..
};
The Detach method would look like this and could be put anywhere you like (most likely a static helper class):
public static void Detach(object obj, string eventName)
{
var caller = new StackTrace().GetFrame(1).GetMethod();
var type = obj.GetType();
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
{
if (typeof(Delegate).IsAssignableFrom(field.FieldType))
{
var handler = (field.GetValue(obj) as Delegate)?.GetInvocationList().FirstOrDefault(m => m.Method.Equals(caller));
if (handler != null)
{
type.GetEvent(eventName).RemoveEventHandler(obj, handler);
return;
}
}
}
}
So for your example the code would look like this:
file.Changed += (s, e) =>
{
Detach(file, nameof(file.Changed));
// My action code here
};
In this case, it's okay.
ReSharper simply warns that handler is different between the time you declare and the time it is executed.
Not sure how exactly you want to design your extension method, but maybe this will get you started:
public static void OnChangedOnce(this FileSystemWatcher instance, Action<object, FileSystemEventArgs> action)
{
var file = instance;
var wrappedAction = action;
FileSystemEventHandler handler = null;
handler = (object sender, FileSystemEventArgs args) =>
{
wrappedAction(sender, args);
file.Changed -= handler;
};
file.Changed += handler;
file.EnableRaisingEvents = true; // mandatory
}
var file = new FileSystemWatcher("path/to/file");
file.OnChangedOnce((sender, args)) =>
{
// your action here
});

How to programmatically raise SizeChanged event

I'm trying to execute code in a SizeChangedEventHandler but the following is not working:
[TestMethod]
public void TestSizeChanged()
{
var panel = new System.Windows.Controls.StackPanel();
bool handled = false;
panel.SizeChanged += (o, e) =>
{
handled = true; // how to get this to be executed
};
panel.Width = 100; // naive attempt to change size!
Assert.IsTrue(handled);
}
I originally tried to use the RaiseEvent method but I was not been able to supply it with the correct xxxEventArgs type, due to not knowing the constructor arguments and the object browser is not helping:
panel.RaiseEvent(new System.Windows.SizeChangedEventArgs()) // does not compile
Obviously, the above test serves no purpose but I'm after correct way of getting the event to fire in a unit-tested environment.
It's very strange that the SizeChanged event doesn't fire with your code, it appears to be correct. Maybe the StackPanel doesn't exists in the visual tree because it's not really shown on the screen, so the event is never fired.
Try to show a real window with a StackPanel on the screen, and programmatically change his width or height.
[TestMethod]
public void TestSizeChanged()
{
Window wnd = new Window();
wnd.Content = new System.Windows.Controls.StackPanel();
bool handled = false;
wnd.SizeChanged += (o, e) =>
{
handled = true; // how to get this to be executed
};
wnd.Show();
wnd.Width = 100; // naive attempt to change size!
Assert.IsTrue(handled);
}
You can't use the RaiseEvent method, because SizeChanged is not a RoutedEvent.
Using the below reflection you can succeed:
//panel =>System.Windows.Controls.Panel instance..
SizeChangedInfo sifo = new SizeChangedInfo(panel, new Size(0, 0), true, true);
SizeChangedEventArgs ea = typeof(System.Windows.SizeChangedEventArgs).GetConstructors(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).FirstOrDefault().Invoke(new object[] {(panel as FrameworkElement),sifo }) as SizeChangedEventArgs;
ea.RoutedEvent = Panel.SizeChangedEvent;
panel.RaiseEvent(ea);

Call action from another action

I have this code;
Button button = new Button();
MessageBox ms = new MessageBox(button);
Action<bool> action = ms.Show();
action += (b) =>
{
Console.WriteLine(b.ToString()); //this isnt working
Console.WriteLine("??");
};
Console.Read();
button.OnClick();
Console.ReadKey();
MessageBox class :
class MessageBox
{
Button _button;
public MessageBox(Button button) { _button = button; }//initialize button
public Action<bool> Show()
{
Action<bool> action = new Action<bool>(CallForTest);
_button.OnClick+=()=>{ action?.Invoke(true); };
return action;
}
//...working.*//
public void CallForTest(bool statu){}
}
I want to return an action and when button is clicked,call the action.But this isnt working? What is the problem? Action is a delegate so delegate is a reference type?(compiler generated class) What is wrong in this picture?
I think when "Show()" is ends,"action" is collected from gargabe collector.But this is working with other reference types? for example;
public Test Show()
{
Test test = new Test("??");
button.OnClick += () =>
{
test.JustForTest(); //working (cw("?????" + ctorvalue);
};
return test;
}
Delegates are immutable. When you are combining two delegates using +=, you are actually creating a new delegate. So when you have done act += ... in the above code, you have actually created a new delegate, it is different from what you have already created in Show() method.
I believe this is happening because when you use += to a delegate it does not append to the internal list. This is why you don't see b.string() being printed
Without changing your design you won't be able to append the action to the original delegate when the button is clicked.
What you are actually writing is somthing like:
var act2 = new Action<bool>((b) =>
{
Console.WriteLine(b.ToString()); //this isnt working
Console.WriteLine("??");
});
var act = act + act2;
as you can see act is getting a new reference to the combined expression of act + act2 rather than act itself concatenating act2 internally.
if you do act(false) you will see the extra results, but not if you invoke the button click.
What you should be using is event on the delegate within the Button, which is the way UI controls are written
class Button
{
public event EventHandler<BoolEventArgs> Click;
}
best to read up on using events when you want to have multicast delegates in this way. MSDN site

remove all EventHandlers of a specific Control

I'm writing an application in winForm. I have a panel in from1 and it has many event handler.
When I dispose panel1 and create new panel , the previous events exist & they fire. For removing panel1 events I have tryed code below.
panel1.Click -= clickHandle_1 ;
But it doesn't work every where in program code. for example in another method.
How can I remove all previous events of panel1 when I create new panel1 ?
According to this,
for cancelling all Click Events of panel1 do this:
FieldInfo f1 = typeof(Control).GetField("EventClick", BindingFlags.Static| BindingFlags.NonPublic);
object obj = f1.GetValue(panel1);
PropertyInfo pi = panel1.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
EventHandlerList list = (EventHandlerList)pi.GetValue(panel1, null);
list.RemoveHandler(obj, list[obj]);
And for cancelling other events of panel1 change EventClick to event name that you want to remove.
One can get all event names using this code:
EventInfo[] info = type.GetEvents();
for (int i = 0; i < info.Length; i++)
{
Console.WriteLine(info[i].Name + "\n");
}
Normally when you Dispose an object/control, all the members of that object/control including Event handler list should be disposed too and unusable. So when you say your panel1 is disposed, it's impossible to fire any event on that disposed control (such as Resize). You should check if you surely dispose your panel. Here is a way you can use to remove all the Event handlers of a control using Reflection to Dispose the Events (type of EventHandlerList):
public void RemoveHandlerList(Control c){
EventHandlerList list = (EventHandlerList) typeof(Control).GetProperty("Events", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(c, null);
typeof(EventHandlerList).GetMethod("Dispose").Invoke(list, null);
}
Remove them one at a time:
panel1.Click -= clickHandle_1;
There is no clean way of unsubscribing all event handlers at once. Keep track of what you're subscribing and unsubscribe them accordingly.
Not sure if this will work but this is closest I came with:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Dictionary<string, EventHandler> _handlers = new Dictionary<string, EventHandler>();
TextBox _txt = new TextBox();
void WireHandlers()
{
// get references of your handlers
EventHandler _hnd1 = delegate { return; }; // here will be your named method. This is only for example
EventHandler _hnd2 = delegate { return; }; // here will be your named method. This is only for example
// wire handlers
_txt.Click += _hnd1;
_txt.TextChanged += _hnd2;
// save wired handler collection
_handlers.Add("Click", _hnd1);
_handlers.Add("TextChanged", _hnd2);
}
void UnwireHandlers()
{
// lets unwire all handlers
foreach (var kvp in _handlers)
{
// inspect keyValuePair - each key corresponds to the name of some event
switch (kvp.Key)
{
case "Click":
_txt.Click -= kvp.Value;
break;
case "TextChanged":
_txt.TextChanged -= kvp.Value;
break;
default:
throw new Exception("no such handler found");
}
}
_handlers.Clear();
}
}

Are Event Handlers removed from memory if the control linked to the event is removed?

In WPF I have created a control that dynamically creates buttons for me. On some occasions the buttons may change and need be recreated. I am currently using the following:
public void GenerateButtons()
{
WrapPanel_Main.Children.Clear();
foreach (ActivatedItem thisItem in Controller.ItemList.Where(sl => sl.IsTypeCompatible(typeof(ActivatedItem))))
{
Button newButton = new Button() { Content = thisItem, ToolTip = thisItem.Desc, Width = 50, Height = 25 };
newButton.Click += new System.Windows.RoutedEventHandler(this.DynamicButtonClick);
WrapPanel_Main.Children.Add(newButton);
}
}
I am wondering if the WrapPanel_Main.Children.Clear(); section of my code is enough to remove the buttons and the events from memory or if I am leaving stuff (like the event handlers) floating around out there?
As always I am open to suggestions on bettering the code shown above as well.
In short, you don't need to worry about this.
When you attach an event handler to the button that event handler isn't keeping the button alive, the button is keeping whatever is referenced by the event handler alive. The event handler is referencing your window, so essentially you can't clear the window from memory until the button leaves memory. Since it doesn't really make sense for the button to live longer than the window this won't happen.
In other words, the situation you need to watch out for is where the items used in the event handler need to have a shorter life than the class that owns the event.
As #Servy mentioned its probably not a needed to detach these handlers, but if you really want to the only way I could think to do this is with Reflection.
Here is a small example based on your question (Button click event for buttons in WrapPanel)
public void RemoveButtonClickHandlers(UIElementCollection elements)
{
foreach (var button in elements.OfType<Button>())
{
try
{
var handlers = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(button, null);
if (handlers != null)
{
var clickEvents = (RoutedEventHandlerInfo[])handlers.GetType()
.GetMethod("GetRoutedEventHandlers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Invoke(handlers, new object[] { ButtonBase.ClickEvent });
foreach (var clickEvent in clickEvents)
{
button.Click -= (RoutedEventHandler)clickEvent.Handler;
}
}
}
catch (Exception ex)
{
// :(
}
}
}
Usage:
RemoveButtonClickHandlers(WrapPanel_Main.Children);
WrapPanel_Main.Children.Clear();

Categories

Resources