How to call a callback function on a GUI event? Delegates? - c#

I'm very new to C# and I'm wondering if using delegates is the right way here:
I created a UserControl in Visual Studio Windows Forms Designer.
In a TableLayoutPanel I have 3 x 3 of these UserControls. Each of them gets a row and col index through the constructor.
Now in my Form that contains the TableLayoutPanel I want to call a function whenever one of the UserControls is clicked and have the row and col index in that function call.
I know how to process the Click event in the UserControl. But I don't know how I can call some kind of callback that I can register with the UserControl.
So the UserControl's Click event handler would call something like Callback(Row, Col);
But I don't know how to get method of my Form into the UserControl. In C I would use a function pointer. Do I need a delegate here?
So in my UserControl partial class I would have something like:
public delegate void DoubleClickHandler(int row, int col);
public DoubleClickHandler Callback;
public void On_Click(object sender, EventArgs e)
{
Callback(Row, Col);
}
And when creating the form I would do something like:
MyControl elem = new MyControl(1, 3);
elem.Callback = delegate (int row, int col) { Console.WriteLine("row {0}, col {1}", row, col); }
It works but I don't know if this is the right way to do it.

Winforms would probably use events for this; same idea, different keywords e.g.
For the event prop
//old
public DoubleClickHandler Callback;
//new
public event EventHandler<(int Row, int Col)> SomethingDoubleClicked; //use a past tense name if you raise after, or a "SomethingDoubleClicking" if you raise before - probably hard to raise a click event before but..
OnClick might have code like
var eh = SomethingDoubleClicked;
eh?.Invoke(this, (theRow, theCol));
And subscription might look like:
//"modern"
yourcontrol.SomethingDoubleClicked += (s, e) => Console.WriteLine("row {0}, col {1}", e.Row, e.Col);
//"classic"
yourcontrol.SomethingDoubleClicked += SomeControl_SomethingDoubleClicked;
void SomeControl_SomethingDoubleClicked(object sender, (int Row, int Col) e)
{
Console.WriteLine("row {0}, col {1}", e.Row, e.Col);
}
Note the += rather than = - an event is conceptually a list of methods that shall be invoked in an undefined order
Feel free to vary that tuple in the EventHandler to be a class or something.. Could also derive from EventArgs, but doesn't have to.
If you have no userstate to report, you can simplify to using:
public event EventHandler SomethingDoubleClicked;
//onclick
var eh = SomethingDoubleClicked;
eh?.Invoke(this, EventArgs.Empty);
ourcontrol.SomethingDoubleClicked += (s, e) => ...;
But I would perhaps avoid deriving from EventArgs to provide custom user state, and then using the EventHandler form immediately above - it'll work because when you do:
eh?.Invoke(this, new MyCustomEventArgs(...));
MyCustomEventArgs derives from EventArgs so you can pass the child class in the parent type, but it means the dev that uses it will have to cast it back:
void SomeControl_SomethingDoubleClicked(object sender, EventArgs e)
{
var mcea = e as MyCustomEventArgs;
}
and it's a bit nasty; better to use EventHandler<MyCustomEvenArgs> to make using it a TAB TAB/no casting affair

Related

Passing control's MouseClick event to its container

I'll try to be concise but comrehensible. I have a dynamic 2-dimensional array of PictureBox elements, and they're all added to the same form via:
this.Controls.Add(PictureBoxArray[i,j]);
Now I've designed an algorithm that would determine which of these PBs are clicked, but I've placed it in the ParentForm_MouseClick method. And now I've reached a paradox. The algorithm I've created returns the proper output, but the ParentForm_MouseClick method is called only when I click on the empty space in the Form, not when I click on PictureBoxes. So my question is - how can I invoke ParentForm_MouseClick method when users click anywhere in the form i.e. can I somehow override PictureBoxes' MouseClick events so that ParentForm_MouseClick is invoked instead?
EDIT: This just occured to me, could I create a custom PictureBoxClass Class that extends the .NET one and just override the MouseClick() event to invoke a method I've previously written?
It seems like you're over-complicating things. If you're going to need to call the code within an event handler from different places, it's a good idea to abstract that out to a different method. You can attach the same event handler to the PictureBoxes which then call that method.
for(int i = 0; i < PictureBoxArray.GetLength(0); i++)
{
for(int j = 0; j < PictureBoxArray.GetLength(1); j++)
{
//attach handler
PictureBoxArray[i,j].Click += pictureBox_Click;
}
}
void pictureBox_Click(object sender, EventArgs e)
{
MyMethod();
}
void parentForm_Click(object sender, EventArgs e)
{
MyMethod();
}
private void MyMethod()
{
//method to be called
}

Using Event Handlers for Multiple Objects

I have 20 items in a List<myObject>. Each has an instance of a UserControl associated with it. Each object is accessible via a MenuStrip that needs to display the UserControl when the appropriate item is clicked. Currently I have an event handler for each of them, which works, but I was wondering if a way exists to simplify this and use a single event handler for all of the items.
Is this possible? If so what is the best way to go about doing so.
EDIT:Can anybody else provide any input on this issue? I'm having trouble with Mailo's answer. Essentially all I need to do is make an event handler that can display the appropriate UserControl stored in a List<myObject> as a property when the correct MenuStrip item is clicked. Is there a more straightforward way to do this? Ideally I'd like to make it so that a foreach loop can go through the list and set up the handlers.
Is there nobody who can help me with this?
It's not very difficult. First is you need some way to associate a menu item with a control in the list.
1) Since you have a list, index is simplest way ( you could use Dictionary<> to simplify this association). So, lets say when you click the first menu item, you want myObjecList[0] to appear. When you click second MenuItem, myObjectList[1] would appear and so on. For this go to each menu item, and in the Properties, assign a value to Tag property. For first menu item, assign Tag to 0, for second item, assign Tag to 1 - and so on.
2) Create one event handler and assign the same handler to all menu items. The event handler could look something like this:
private void myToolStripMenuItem_Click(object sender, EventArgs e)
{
// source menu item which was clicked
ToolStripMenuItem item = sender as ToolStripMenuItem;
if(item != null)
{
int index = int.Parse(item.Tag.ToString()); // get the index from Tag
myObject control = myObjectList[index];
// do your stuff with your control
}
}
you can pass the sender object to your event handler and check for the type inside the handler
it will be something like this
//this will contain any properties you wanna send to the handler
public class MyHandlerEventArgs : EventArgs
{
}
//this delegate gets the sender, you can change the sender type to be the encapsulated type of your controls
public delegate void MyHandler(object sender, MyHandlerEventArgs args);
//this is the class that fires the event in your case it will UI class I think
public class MyController
{
public event MyHandler myEvent;
public void MyEvent_Fire()
{
if(myEvent != null)
myEvent(this, new MyHandlerEventArgs());
}
}
//here you can do your business logic for each control
public class MyAction
{
MyController mc = new MyController();
public MyAction()
{
mc.myEvent += new MyHandler(mc_myEvent);
}
void mc_myEvent(object sender, MyHandlerEventArgs args)
{
//check the sender type
//do your action
}
}
this link might be useful to
check
You might want the ItemClicked event from the MenuStrip. You can use the ClickedItem from the ToolStripItemClickedEventArgsto find your UserControl
Some pseudocode:
// Initialize
myMenuStrip.ItemClicked += itemClickedEvent;
// ...
void itemClickedEvent(Object sender, ToolStripItemClickedEventArgs e)
{
int index = myObjectList.FindIndex(e => e.instanceOfUserControl == e.ClickedItem);
// Now that we have the clicked item, display it how we would in an individual event handler.
myObjectList[index].instanceOfUserControl.DoDisplay();
}
Ideally I'd like to make it so that a foreach loop can go through the list and set up the handlers.
For this approach you'd use a foreach on the List
foreach (var listItem in myObjectList)
{
listItem.TheEvent += myEventHandler;
}

How to sync up to different events in Winforms

If I have a button which does something and also a double-click event on a data grid which I want to do the same thing, what is the best way to ensure that only one function has to be maintained?
Apart from doing the following, is there any fancy C# way to indicate that two events are to do the same thing?
void button1_Click(...) { MyFunction(); }
void dataGrid1_DoubleClick(...) { MyFunction(); }
void MyFunction() { // do stuff }
I suppose that you are talking about a DataGridView (WinForms) so the signature of the event DoubleClick in the DataGridView and the signature of Click event on a button control is the same.
(An EventHadler). In this case you can simply set the same method using the form designer or manually bind the event
dataGridView1.DoubleClick += new EventHandler(MyFunction);
button1.Click += new EventHandler(MyFunction);
Of course the MyFunction method should match the expected signature of an EventHandler
private void MyFunction(object sender, EventArgs e)
{
// do your work
}
Reviewing my answer after a few minutes I wish to add:
If you find yourself in a situation in which you need to differentiate between the controls using the sender object (like Control c = sender as Control; if (c.Name == "someName") ) I really suggest you to return to the first idea. Call a common method but keep the EventHandler separated for each control involved.
Using VS, in the form's designer view You can set the procedure You want to call to each control's each event in the control's properties window.
image
Just to add to what Steve said, you will want to bind these events to your function manually in the Load event of your form, instead of using the events under the lightning bolt in the properties window in the designer, like so:
private void Form1_Load(object sender, EventArgs e)
{
button1.Click += MyMethod;
dataGridView1.DoubleClick += MyMethod;
}
void MyMethod(object sender, EventArgs e)
{
//Do Stuff
}
Also, declaring a new instance of the EventHandler class has been redundant since Anonymous methods were introduced to C#, you can just point the event directly at the method as shown above.

Is it ok practice to use a static class to collect common event handlers?

In a Windows Forms app, there are some things I want various controls of the same type on different forms to always do upon certain events. For example, I want a single click on a DataGridViewCell that is a TextBox-type and is not read only to automatically enter edit mode. The simple event handler code, in this case on a form with a DataGridView called dgvParms, is:
private void dgvParms_CellClick(object sender, DataGridViewCellEventArgs e) {
DataGridViewCell c = dgvParms[e.ColumnIndex, e.RowIndex];
if (!c.ReadOnly && c is DataGridViewTextBoxCell) {
dgvParms.CurrentCell = c;
dgvParms.BeginEdit(true);
}
}
I could easily move this method to a static class, say CommonEvents, and then on my individual forms' Load handlers assign the static method definition to the respective DataGridViews' CellClick event
this.dgvParms.CellClick += CommonEvents.dgvEditOnCellClick;
Is this acceptable or good practice? Or is it preffered to keep event handler logic with each consuming form's code? I could of course do something in between (but redundant) by defining local event handlers which then call the common method, such as
this.dgvParms.CellClick += (a, b) => CommonEvents.dgvEditOnCellClick(a, b);
You could try defining your own customized DataGridView by inheriting from DatagridView, and then adding this and any other specialized behaviors you need. I can't test the code below at this moment, but I believe it is correct:
public class MyDgv : DataGridView
{
protected override void OnCellClick(DataGridViewCellEventArgs e)
{
DataGridViewCell c = this[e.ColumnIndex, e.RowIndex];
if (!c.ReadOnly && c is DataGridViewTextBoxCell)
{
this.CurrentCell = c;
this.BeginEdit(true);
}
}
}
You may have some reason not to do this, but if you like this specific behavior in all of your dgv controls, just use your custom implementation.
This looks absolutely fine to me, although of course you will have to change you code to use the sender argument to locate the DataGridView that raised the event:
private void dgvParms_CellClick(object sender, DataGridViewCellEventArgs e) {
DataGridView dgvParams = sender as DataGridView;
DataGridViewCell c = dgvParms[e.ColumnIndex, e.RowIndex];
if (!c.ReadOnly && c is DataGridViewTextBoxCell) {
dgvParms.CurrentCell = c;
dgvParms.BeginEdit(true);
}
}
The general approach of changing or adding behaviour to a control by handling and reacting to its events is called an attached behaviour. It is most typically used in WPF, but there is nothing wrong with using it in WinForms too.

Hooking up generic event handlers to multiple controls of the same type

I have a WinForms app that contains many NumericUpDown controls. In a nutshell, if my users enter a value into the control and then delete the text, I want to restore it (the text) when the control loses focus. So I decided that I'd check .Text when the control loses focus and if it's empty, I set .Text = .Value.ToString().
I'm doing this in the Leave event handler and it works just fine. But as I said, I have many of these controls (18, to be exact). I don't like creating 18 Leave event handlers that all do the same thing so I created a generic one like this:
private void numericUpDown_GenericLeave(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(((NumericUpDown)sender).Text))
((NumericUpDown)sender).Text = ((NumericUpDown)sender).Value.ToString();
}
I started to hook up all of the controls to this generic event handler but I quickly got tired of doing this:
numericUpDown1.Leave += numericUpDown_GenericLeave;
numericUpDown2.Leave += numericUpDown_GenericLeave;
numericUpDown3.Leave += numericUpDown_GenericLeave;
...
numericUpDown18.Leave += numericUpDown_GenericLeave;
So I thought I'd create a function that would return a list of all the controls of a specified type and then loop through that list and hookup the event handlers. That function looks like this:
public static List<Control> GetControlsOfSpecificType(Control container, Type type)
{
var controls = new List<Control>();
foreach (Control ctrl in container.Controls)
{
if (ctrl.GetType() == type)
controls.Add(ctrl);
controls.AddRange(GetControlsOfSpecificType(ctrl, type));
}
return controls;
}
I call the function like this:
var listOfControls = GetControlsOfSpecificType(this, typeof(NumericUpDown));
foreach (var numericUpDownControl in listOfControls)
{
numericUpDownControl.Leave += numericUpDown_GenericLeave;
}
When I run my app, however, I don't see the expected behavior that occurs when I manually hookup each control to the generic event handler. This code is currently in the constructor of my form and I've tried calling it before as well as after the call to InitializeComponent() but neither one seems to be working. I get no error of any kind, I just don't see the behavior that I was expecting. I have a breakpoint set inside the generic event handler but the debugger never breaks so it seems like the event handler isn't being hooked up correctly. Does anyone know why this might be or how I can troubleshoot it further? Thanks!
EDIT
I just realized that the call to:
var listOfControls = GetControlsOfSpecificType(this, typeof(NumericUpDown));
was happening before the call to InitializeComponent() so of course the list of controls being returned was empty. DOH! Thanks for all the replys. I apologize for wasting everyones time. :-(
You're passing this to your method, which is presumably a reference to your form. Your method will only catch the controls that are placed directly on your form. Any NumericUpDown controls that are not directly on the form (i.e. they're sitting on a panel or something) will be missed.
Why not create a user control that has a NumericUpDown control in it.
Then handle this is in the user control events.
This worked for me:
private decimal _previous = 0;
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
if (((NumericUpDown)sender).Text.Length > 0)
{
_previous = this.numericUpDown1.Value;
}
}
private void UserControl1_Leave(object sender, EventArgs e)
{
if (this.numericUpDown1.Text == "")
{
this.numericUpDown1.Value = _previous;
this.numericUpDown1.Text = System.Convert.ToString(_previous);
}
}
Just note that the Leave event is on the user control not on the updown control itself.
Question answered. See Edit above. Thanks to bsegraves for pointing me in the right direction.

Categories