c# winform delegates for n subforms - c#

I have a C# main form that opens up C# sub forms in separate tabs. Each tab is simply an instance of the same sub form so the tab code is like:
SubForm sf = new SubForm();
TabPage tp = new TabPage();
tp.Controls.Add(sf);
tabControl.TabPages.Add(tp);
There can be n tabs and subforms. Then each new subform instance has a delegate to handle external event updates, like so:
public partial class SubForm : Form
{
... form setup ...
internal void DoStuff(value v)
{
if (InvokeRequired)
{
// Generic Action delegate
Invoke(new Action<string, string>(DoStuff), value);
return;
}
myLabel.Text = value;
Show();
}
}
Click the subscribe button and there's a Geode registration to specific keys in the subform, and the delegate is passed as an event handler:
private void button_Click(object sender, EventArgs e)
{
new Geode().RegisterMyListener(cache, key, DoStuff);
}
When the Geode key value is updated then the update is handled.
This is working fine for the 1st subform. Then with a 2nd or 3rd subform all the Geode subscriptions to each subform's keys are updating, but all the updates are being handled only by the most recently instantiated subform's delegate. I had not expected that to happen because doesn't each new subform instance have its own stack with a new delegate?
UPDATE: Once a 2nd key is registered with Geode RegisterMyListener like this:
region.GetSubscriptionService().RegisterKeys(s);
region.AttributesMutator.SetCacheListener(new Listener<string, string>(DoStuff));
then every event update references the latest DoStuff delegate and never a previous one. So is a Geode listener a static register? I am looking to be able to subscribe to separate keys with many instances of the same Listener. Is that possible? Or am I going to need multiple listeners for multiple subforms?

You can do it with extension method like this:
public static class Extensions
{
public static void InvokeAction(this Control control, Action action)
{
if (control.InvokeRequired)
{
control.Invoke(new MethodInvoker(() => { action(); }));
}
else
{
action();
}
}
}
Usage:
public partial class SubForm : Form
{
public void SetExampleText(string text)
{
this.InvokeAction(() => { this.ExampleTextBox.Text = text; })
}
}

Related

Is there a way to activate a Button that exists within another class?

I am using C# and Xamarin. I have two separate classes. One class is essentially the user interface and another class is acting as a custom built generic entry for users to input data and search for results by clicking a button.
Main UI Class:
Class MainPage
{
public MainPage
{
Content = new StackLayout
{
Children =
{
new InputClass // This is my custom built user entry class
{
}.Invoke(ic => ic.Clicked += WhenButtonPressedMethod) // The problem is here, I can't figure out how to call the button within the input class to fire a clicked event.
}
}
}
}
public async void WhenButtonPressedMethod (object sender, EventArgs e)
{
// Supposed to do stuff when the button is pressed
}
InputClass:
public class InputClass : Grid
{
public delegate void OnClickedHandler(object sender, EventArgs e);
public event OnClickHandler Clicked;
public InputClass
{
Children.Add(
new Button {}
.Invoke(button => button.Clicked += Button_Clicked)
)
}
private void Button_Clicked(object sender, EventArgs e)
{
Clicked?.Invoke(this, e);
}
}
The "InputClass" is a grid that holds a title text label, an entry and a button that a user can press to submit and search data. The button in this class is what I'm trying to actually access to invoke/cause a click event so that the method in the main UI class can be called. But, when I try to invoke a click event on the "InputClass" I can't access the button inside of it, I can only access "InputClass" itself which is just a grid with no useful event properties.
Any solutions or ideas?
If you are running into the same problem as mentioned here, follow the code on this page and read through the comments, it covers enough to be able to piece it together. My mistake was attaching Invokes to the wrong objects.
Don't know why fluent Invoke didn't work correctly.
Add the event handlers this way:
public MainPage
{
var ic = new InputClass();
ic.Clicked += WhenButtonPressedMethod;
Content = new StackLayout
{
Children = { ic }
}
}
public InputClass
{
var button = new Button;
button.Clicked += Button_Clicked;
Children.Add(button);
}

Raising and handling events in a multi window UWP app

I created some test code so I could try and figure out how to use multiple windows in UWP properly. I wanted to see if I could fire an event and have multiple windows update their UI in the event handler. I finally got something working but I'm not entirely sure why it works.
Here's the class that's being created in my Page
public class NumberCruncher
{
private static Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>> StaticDispatchers { get; set; }
static NumberCruncher()
{
StaticDispatchers = new Dictionary<int, Tuple<CoreDispatcher, NumberCruncher>>();
}
public NumberCruncher()
{
}
public event EventHandler<NumberEventArgs> NumberEvent;
public static void Register(int id, CoreDispatcher dispatcher, NumberCruncher numberCruncher)
{
StaticDispatchers.Add(id, new Tuple<CoreDispatcher, NumberCruncher>(dispatcher, numberCruncher));
}
public async Task SendInNumber(int id, int value)
{
foreach (var dispatcher in StaticDispatchers)
{
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
dispatcher.Value.Item2.NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
}
}
}
And here's the relevant part of my MainPage code
NumberCruncher numberCruncher;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NumberCruncher.Register(ApplicationView.GetForCurrentView().Id, Window.Current.Dispatcher, numberCruncher);
}
I have a button that creates new views of the MainPage. Then I have another button that calls the SendInNumber() method.
When I navigate to the MainPage I register the Dispatcher for the window and the instance of NumberCruncher. Then when firing the event I use the NumberCruncher EventHandler for that specific Dispatcher.
This works without throwing marshaling exceptions. If I try to use the current class's EventHandler
await dispatcher.Value.Item1.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Debug.WriteLine($"invoking {dispatcher.Key}");
NumberEvent?.Invoke(null, new NumberEventArgs(id, value));
});
I get a marshaling exception when trying to add the item to the listView. However if I maintain the SynchronizationContext in my MainPage and then use SynchronizationContext.Post to update the listView. It works fine
SynchronizationContext synchronizationContext;
public MainPage()
{
this.InitializeComponent();
numberCruncher = new NumberCruncher();
numberCruncher.NumberEvent += NumberCruncher_NumberEvent;
synchronizationContext = SynchronizationContext.Current;
}
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
synchronizationContext.Post(_ =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
}, null);
}
However this does not work and throws a marshaling exception when trying to update listView.
private async void NumberCruncher_NumberEvent(object sender, NumberEventArgs e)
{
await CoreApplication.GetCurrentView().CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
listView.Items.Add($"{e.Id} sent {e.Number}");
});
}
What is going on here?
Important thing to remember is that when an event is fired, the subscribed methods are called on the same thread as the Invoke method.
If I try to use the current class's EventHandler, I get a marshaling exception when trying to add the item to the listView.
This first error happens because you are trying to fire the event of current class on another Window's dispatcher (dispatcher.Value.Item1). Let's say the event is on Window 1 and the dispatcher.Value.Item1 belongs to Window 2. Once inside the Dispatcher block, you are or the UI thread of Window 2 and firing the NumberEvent of Window 1's NumberCruncher will run the Window 1's handler on the Window 2's UI thread and that causes the exception.
However this does not work and throws a marshaling exception when trying to update listView.
The GetCurrentView() method returns the currently active view. So whichever application view is active at that moment will be the one returned. In your case it will be the one on which you clicked the button. In case you call the Invoke method on the target Window's UI thread, you don't need any additional code inside the NumberEvent handler.

Unsure of the actual implementation of events/delegates across multiple classes

I've read through several pages here on Events and Delegates and understand the idea behind them but am unsure of how to use them across multiple classes. Until now, I've simply relied on the IDE to set everything up for me and I didn't realize it worked inside a single class alone.
public class MyForm : Form
{
public MyForm()
{
...
this.Controls.Add(menuBuilder.GenerateMenuForMyForm());
//load other controls into the form to visualize/manipulate data
}
public void UpdateDataInControls()
{
//reloads info into controls based on data in serializable class.
}
}
public class MenuBuilder
{
public MenuStrip GenerateMenuForMyForm()
{
MenuStrip menu = new MenuStrip();
...
ToolStripMenuItem loadfile = new ToolStripMenuItem();
loadfile.name = "loadfile";
loadfile.text = "Load File";
loadfile.Click += new EventHandler(loadfile_Click);
file.DropDownItems.Add(loadfile);
...
return menu;
}
void loadfile_Click(object sender, EventArgs e)
{
//Open a file dialog and deserialize file
//Need to send an event to MyForm letting it know that it needs to
//update controls in the form to reflect the deserialized data.
}
}
So in this instance, I have events working within a single class, but I'm unsure how to set things up so MyForm can receive an event from MenuBuilder. I have tried something like
loadfile.Click += new EventHandler(myFormObject.loadfile_Click);
and make the loadfile_Click() function in MyForm, but that seems counter-intuitive to the idea of driving functionality through events themselves since it needs the form's object itself to be passed into the constructor. If that's the case, I might as well just call the function directly.
Here is one simple way to achieve what your'e looking for. Its a basic event model where a class declares its events, and an observer class subscribes to it.
using System;
using System.Windows.Forms;
public class MyForm : Form
{
public MyForm()
{
var menuBuilder = new MenuBuilder();
menuBuilder.FileLoaded += (sender, args) => UpdateDataInControls();
Controls.Add(menuBuilder.GenerateMenuForMyForm());
//load other controls into the form to visualize/manipulate data
}
public void UpdateDataInControls()
{
//reloads info into controls based on data in serializable class.
}
}
internal class FileLoadedEventArgs : EventArgs
{
// customize event arguments if need be
// e.g. public string FileName {get;set;}
}
public class MenuBuilder
{
// declare event delegate
internal delegate void FileLoadedEvent(object sender, FileLoadedEventArgs e);
// declare event for observers to subscribe
internal event FileLoadedEvent FileLoaded;
public MenuStrip GenerateMenuForMyForm()
{
MenuStrip menu = new MenuStrip();
/*
ToolStripMenuItem loadfile = new ToolStripMenuItem();
loadfile.name = "loadfile";
loadfile.text = "Load File";
loadfile.Click += new EventHandler(loadfile_Click);
file.DropDownItems.Add(loadfile);
*/
return menu;
}
void loadfile_Click(object sender, EventArgs e)
{
// fire the event
FileLoaded(this, new FileLoadedEventArgs());
}
}

Windows Forms - pass argument between form and control

In my C# Windows Forms project I have:
mycontrol (it's keyboard)
myform (it's layout for textbox, and mycontrol )
I would like to run some code which is in myform, but by pressing button in mycontrol )
For example:
When I press Backspace button in mycontrol I just use Button Event
SendKeys.Send("{BACKSPACE}");
and myform textbox know that I press Backspace.
But I have some custom buttons (functional) and those buttons should be define in myform
for example in myform I have:
private void btnOK_Click(object sender, EventArgs e)
{
DoSomething();
}
The whole problem is - how to Run myform btnOK_Click or DoSomething from mycontrol
UPDATE #2:
Nothing happens while pressing D0, NullReference when I delete checking if it's null.
internal partial class myForm : BaseForms
{
public myForm() {
InitializeComponent();
ShowMyControl();
}
private void ShowMyControl(){
KeyboardControl myControl = new KeyboardControl();
myControl.KeyboardKeyPressed += new Action<string>(OnMyControlKeyPressed);
this.Controls.Add(myControl);
}
private void OnMyControlKeyPressed(string key)
{
switch (key)
{
case "D0":
MessageBox.Show("A");
break;
case "D1":
MessageBox.Show("B");
break;
default:
MessageBox.Show("C");
break;
}
}
...
}
and
public partial class KeyboardControl : UserControl
{
public event Action<string> KeyboardKeyPressed;
...
private void HandlingMouseClick1(Point PressedItem)
{
...
case Keys.D0:
if (KeyboardKeyPressed != null)
KeyboardKeyPressed("D0");
break;
}
}
In MyForm_Load, or anywhere else if you dynamically initialize your MyControl, you can add something like MyControl.Click += new System.KeyEventHandler(this.MyControl_Click) and place a method called
private void MyControl_Click(sender object, KeyEventArgs e)
{
... //Find out which key was pressed, proceed.
}
in MyForm. The method will be called when the Click event is raised.
Try out the below approach
inside MyForm
public class MyForm : Form
{
//.ctor
MyForm() { }
private void ShowMyControl(){
MyControl myControl = new MyControl();
myControl.KeyboardKeyPressed += new Action<string>(OnMyControlKeyPressed);
this.Controls.Add(myControl);
}
private void OnMyControlKeyPressed(string key)
{
switch(key)
{
case "D0" :
DoSomething();
break;
case "D1" :
DoSomethingElse();
break;
default :
SendKeys(key);
break;
}
}
}
/*MyControl*/
namespace Keyboards
{
public class MyControl : Control
{
public event Action<string> KeyboardKeyPressed;
private void HandlingMouseClick(Point PressedItem)
{
if(KeyboardKeyPressed != null)
KeyboardKeyPressed(PressedItem.ToString());
}
}
}
Use a delegate and pass it to the constructor of your second form.
public delegate void MyDelegate(Object SomeData);
You'll need to create a delegate according to the method's signature you want to use.
MyDelegate del = form1.DoSomething;
Form2 form2 = new Form2(..., del);
Then you can encapsulate a method from your form1 and pass it to form2 and then invoke it in form2.
public Form2(..., MyDelegate del){ }
del.Invoke();
Look here for delegates.
Your control should expose an event that the form can then subscribe to and act upon.
Give your event a meaningful name. If your control contains a Search button and the user clicks it, let your control fire an event called SearchButtonClicked, for instance.
Then the form can contain a method that subscribes to the SearchButtonClicked event, and that code will then be executed whenever the user clicks the Search button.
See also the Events Tutorial on MSDN.
Add a reference of MyForm in your MyControl class
class MyControl
{
MyForm _form=null;
MyControl(MyForm form)
{
_form=form;
}
void DoSth()
{
if(_form!=null)
_form.DoSomething();
}
}

Pass variable between forms when clicking button

I have two Forms. One with where all the main code is being executed. And the other form is displayed when clicking a menu item by using this method:
Form2 videoSettings = new Form2();
private void videoToolStripMenuItem_Click(object sender, EventArgs e)
{
videoSettings.Show();
}
The form which is then opened containsfields where the user gets to set some settings for the application.
Then when clicking the "save" button I want this variable: public int deviceIndex;
to be fetched from the original Form.
So I'm wondering if I can add any event or something in Form1 which detects when the save button is clicked in videoSettings (Form2)?
I would do it a different way. I'd separate the code between the UI handling and the business logic layers. So your scenario would run in such a way:
The first form issues an event notifying that the button with certain semantics has been activated. The data needed for the processing is included into the event's data.
The business logic listens to this event, and decides to issue a command on the second form. It calls an appropriate method on the form's class, passing the needed information as a parameter (and maybe preprocessing the parameter if needed).
The second form receives the command from the business logic and updates the view.
This way the problem doesn't arise at all.
Example: (I'm not the winforms expert, beware it can be totally wrong from the POV of best winforms practices.)
Part 1 (first form):
class ProcessingActivatedEventArgs : EventArgs
{
public ProcessingActivatedEventArgs(int data) { MoreData = data; }
public int MoreData { get; protected set; }
}
class Form1 : Form
{
private int currentData;
public event EventHandler<ProcessingActivatedEventArgs> ProcessingActivated;
void OnButtonClick(object sender, EventArgs args)
{
// ...
if (ProcessingActivated != null)
ProcessingActivated(new ProcessingActivatedEventArgs(currentData));
}
}
Part 2: (business logic)
class Controller
{
Form1 f1;
Form2 f2;
void StartFirstForm()
{
f1 = new Form1();
f1.ProcessingActivated += OnProcessingActivated;
f1.Show();
}
void OnProcessingActivated(object sender, ProcessingActivatedEventArgs args)
{
int data = args.MoreData;
f1.DisableProcessingRequests();
model.ProcessingFinished += OnProcessingFinished;
model.StartProcessing(data);
if (data > 0)
f2.DisplayDataProcessing(0, data);
else if (data < 0)
f2.DisplayDataProcessing(data, 0);
else
throw new SomeCoolException("impossible data");
}
}
Part 3: (second form)
class Form2 : Form
{
public void DisplayDataProcessing(int lower, int upper)
{
// ... update the UI
}
}
Note that this implementation ties the Controller and forms tighter than it could be done. In WPF, the decoupling is achieved by using the appropriate DataContext (but I don't know how to do it properly in WinForms).
Let me suggest another way, something between the simplest ShowDialog() and the elaborated way of separation between business logic and interface.
I wish to create a new event in Form2. I call this event SettingsSaved
In Form2 add as global declaration
public delegate void SettingsSavedEventHandler(object sender, SettingsSavedEventArgs e);
public event SettingsSavedEventHandler SettingsSaved;
and in the cmdSave_Click event
if(SettingsSaved != null)
{
SettingsSavedEventArgs ss = new SettingsSavedEventArgs() { DeviceIndex = deviceIndex};
SettingsSaved(this, ss);
}
the skeleton for the class SettingsSavedEventArgs
public class SettingsSavedEventArgs: EventArgs
{
public int DeviceIndex {get; set;}
// Other settings could be added here
}
now in the code calling the Form2 we can subscribe to the event and get notified when the user clicks on the Form2 Save button
Form2 videoSettings = new Form2();
videoSettings.SettingsSaved += new SettingsSavedEventHandler(SavedHandler);
videoSettings.Show();
....
private void SavedHandler(object sender, SettingsSavedEventArgs ss)
{
int deviceIndex = ss.DeviceIndex;
}
Observer Pattern
There are many suggestions, but I'd like to add my two cents.
You could use form2.ShowDialog(); which will stop the execution of your form1 thread until the form2 is closed. Which means you can just do this from form1:
Form2 videoSettings = new Form2();
//show options
videoSettings.ShowDialog();
//at this point, the user has either clicked save, cancel, or closed the form
//(because the form is closed, obviously :) )
int device = videoSettings.deviceIndex;
If you cant have it locking up your form like that, here is another way using an event in Form2:
Form2 : Form
{
public event EventHandler Saved;
OnSaveButtonClicked(...)
{
if(Saved != null) Saved(this, new EventArgs());
}
}
and then from Form1:
Form2 frm = new Form2();
frm.Saved += (s, e) =>
{
//saved button clicked, retrieve value.
//also could be handled as a method, or really any way.
};
frm.Show();
Maybe you could try to have your second form to implement INotifyPropertyChanged interface. Then when you click on Save, you Raise the PropertyChanged event, and you capture it in the first form.
You can pass information something like this
private Form1 mainForm = null;
public Form2(Form callingForm)
{
mainForm = callingForm as Form1;
InitializeComponent();
}
Then, you can access the Form1 property from Form2 like this:
//Call this in Save button click event
this.mainForm.deviceIndex;

Categories