C# - Most OOP way to pass data between threads - c#

Let's say I have a thread that waits for a user to click a button before advancing:
System.Threading.AutoResetEvent dialoguePause = new System.Threading.AutoResetEvent(false);
public void AskQuestion()
{
/* buttons containing choices created here */
dialoguePause.WaitOne();
/*Code that handles choice here */
}
public void Choice_Clicked(object sender, EventArgs e)
{
dialoguePause.Set();
}
How can I pass data from the thread Choice_Clicked is on to AskQuestion without relying on class variables? The best I can do is this:
System.Threading.AutoResetEvent dialoguePause = new System.Threading.AutoResetEvent(false);
string mostRecentChoice;
public void AskQuestion()
{
/* buttons containing choices created here */
dialoguePause.WaitOne();
MessageBox.Show("You chose " + mostRecentChoice + ".");
}
public void Choice_Clicked(object sender, EventArgs e)
{
mostRecentChoice = (sender as Button).Content.ToString(); //Ugly!
dialoguePause.Set();
}

There are many ways to achieve this:
Use a property in a Singleton or Monostate. There is always one instance of such property, so regardless which thread writes, and which reads, the will share it, as long as they are in one Application Domain.
Use messaging
If you are in same class, use field or property (look out for cross-threads!)
...
What I am trying to say, it depends on the application. I have no clue if this is WinForm, WPF or Web ...

Related

Multiple thread not working correctly in C#

I create parallel process and DataTable dtUser have two rows, it should create two browser:
Parallel.ForEach(dtUser.AsEnumerable(), items =>
OpenBrowser(items["user"].ToString(), items["pass"].ToString()));
Lapsoft_OneDriver browser;
public void OpenBrowser(string username, string password)
{
browser = new Lapsoft_OneDriver(Browsers.Chrome);
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
}
It create two Chrome process but only first process running line code block:
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
The second process only initializes new browser and not do anything.
If I change this line:
browser = new Lapsoft_OneDriver(Browsers.Chrome);
to
var browser = new Lapsoft_OneDriver(Browsers.Chrome);
It's working.
But another method continues to use variable browser to execute other code.
So, I must declare global variable Lapsoft_OneDriver browser out of a function to use in another method use it.
My problem is:
Why using Lapsoft_OneDriver browser; it create two Chrome process but only first process active, it will insert to browser.FindElementById("txtUserName") two values of variable username and second process not do anything?
Updated:
When to change the code, I have any problem.
I will add more code of frmMain_Load:
private void frmMain_Load(object sender, EventArgs e)
{
thread = new LThread();
thread.StartedEvent += new LThread.startDelegate(AllCaseProgram);
numLog = int.Parse(dtSetting.Rows[0]["num_Log"].ToString());
}
int numProcess;
private void AllCaseProgram(object args)
{
try
{
switch (numProcess)
{
case 0:
Parallel.ForEach(dtUser.AsEnumerable(), items => Start(items["user"].ToString(), items["pass"].ToString()));
break;
case 1:
ClickCart();
break;
case 2:
Result();
break;
}
}
catch (Exception ex)
{
if (browser != null)
browser.Cleanup();
numProcess = 0;
AllCaseProgram(null);
}
}
At event of button StartProgram()_Click. I start Thread like: thread.Start();
You said: should be add this function to my program.
public static void Start(string user, string pwd)
{
var test = new frmMain();
test.OpenBrowser(user, pwd);
test.ClickCart();
}
My update question is:
Seem function Start(string user, string pwd) should be change to function AllCaseProgram include all switch case.
And variable numLog in frmMain_Load have values = 3. In function test.ClickCart() I also use this variable but values auto change to 0.
Have any issues with code? Thanks.
And LThread class is:
public class LThread : BackgroundWorker
{
#region Members
public delegate void startDelegate(string ID);
public event startDelegate StartedEvent;
private static int RandNumber(int Low, int High)
{
Random rndNum = new Random(int.Parse(Guid.NewGuid().ToString().Substring(0, 8), System.Globalization.NumberStyles.HexNumber));
int rnd = rndNum.Next(Low, High);
return rnd;
}
protected override void OnDoWork(DoWorkEventArgs e)
{
StartedEvent(RandNumber(100,10000).ToString()); //put whatever parameter suits you or nothing
base.OnDoWork(e);
e.Result = e.Argument;
}
BackgroundWorker bwThread;
// Main thread sets this event to stop worker thread:
public Boolean bwIsRun;
int m_time_delay = 10000;
Delegate m_form_method_run;
Delegate m_form_method_stop;
Form m_type_form;
#endregion
#region Functions
public void Start()
{
try
{
bwIsRun = true;
this.RunWorkerAsync();
}
catch { }
}
public void Stop()
{
try
{
bwIsRun = false;
}
catch { }
}
private void StartToListen(object sender, DoWorkEventArgs e)
{
while (true)
{
Thread.Sleep(m_time_delay);
if (bwIsRun == true)
{
m_type_form.Invoke(m_form_method_run);
}
else
{
BackgroundWorker bwAsync = sender as BackgroundWorker;
if (bwAsync.CancellationPending)
{
e.Cancel = true;
return;
}
break;
}
}
}
#endregion
}
You should encapsulate your state for each test run. That way you'll have a class that has the responsibility the start a browser, execute one or more actions, while keeping all the required state belonging to a single run private for just one instance, while you can have a many instances as you like (if resources permit).
// this is NOT a winform, this is a new and seperate class ...
// don't try to mix this with an WinForm, that will fail
public class BrowserTestRunner
{
// only this Test instances uses this browser
Lapsoft_OneDriver browser;
private void OpenBrowser(string username, string password)
{
browser = new Lapsoft_OneDriver(Browsers.Chrome);
browser.GoToUrl(link);
browser.FindElementById("txtUserName").SendKeys(username);
browser.FindElementById("txtpassword").SendKeys(password);
// you probably want to click on something here
}
// some other test
private void ClickCart()
{
browser.FindElementById("btnCart").Click();
}
// add other actions here
// this starts the test for ONE browser
public static void Start(string user, string pwd)
{
var runner = new BrowserTestRunner();
runner.OpenBrowser(user, pwd);
// wait for stuff, check data, prepare the next steps
// for example
// runner.ClickCart();
// other actons here
}
}
Now you can create as many Test class instances as you like, while each instance of the class manages its own internal state, without interfering with other instances:
Parallel.ForEach(dtUser.AsEnumerable(), items =>
BrowserTestRunner.Start(items["user"].ToString(), items["pass"].ToString()));
If you want to start that from your backgroundworker do:
private void AllCaseProgram(object args)
{
try
{
switch (numProcess)
{
case 0:
Parallel.ForEach(
dtUser.AsEnumerable(),
items => BrowserTestRunner.Start(items["user"].ToString(), items["pass"].ToString()));
break;
case 1:
ClickCart();
break;
case 2:
Result();
break;
}
}
catch (Exception ex)
{
if (browser != null)
browser.Cleanup();
numProcess = 0;
AllCaseProgram(null);
}
}
By all means: don't start the main form again. Just separate your WinForm from the code you use to operate the browser. That does mean that you have to move the code that interacts with the browser to the BrowserTestRunner. Don't try in keeping the logic for your selenium stuff in the WinForm class because that is doomed to fail. As you are already experiencing.
What you got here is sort of a race condition. You got two threads not getting along when handling a single field in the class. Your problem is only that you don't have sufficient space to store all the browser instances you require.
What happens is basically that the first thread enters the method, creates a instance of the chrome browser and stores it in the variable. Then the second thread enters the function and does the same thing. But it also stores the instance in the same variable. Now the first thread continues and goes to a link. But the instance it is working with is already replaced by the second thread. And so on. This may happen with the threads the other way around or the overlapping may happen after more lines where handled. But it is bound to go wrong.
The way to resolve it, is as you noticed to make the variable local by adding a var. This way both threads are working with distinct variables.
Now you said you need the variable in another function. The question is: Do you need both? Do you need only one? Do you need a specific one?
In case you need only one, you just store the variable in the global variable by adding a line like this in your function:
this.browser = browser;
So it would look like this in total:
Lapsoft_OneDriver browser;
public void OpenBrowser(string username, string password)
{
var localBrowser = new Lapsoft_OneDriver(Browsers.Chrome);
localBrowser.GoToUrl(link);
localBrowser.FindElementById("txtUserName").SendKeys(username);
localBrowser.FindElementById("txtpassword").SendKeys(password);
this.browser = localBrowser;
}
I changed the name of the local browser variable, so it gets clearer what variable is used. Do note that either one of the created browsers could end up in the variable.
In case you need a specific one you have to determine if you have the correct one and store the result after this.
If you need both you have to store them in a list. The namespace System.Collections.Concurrent offers lists that can be handled by multiple threads at once.

UI Forms events block my other STOCK TICKER API events - how can I prevent this?

I have an API (dll) that collects stock ticks via an event mechanism. Such as below:
...
using MT4API;
public partial class Blue : Form
{
...
public Blue()
{
...
string symbol = "GBPUSD";
MT4DDE dde = new MT4DDE("");
dde.OnQuote += new System.EventHandler<QuoteEventArgs>(MT_OnQuote);
dde.Connect();
dde.Subscribe(symbol);
....
The idea is that on each chart tick I get an event. here is the event handler code:
private static void MT_OnQuote(object sender, QuoteEventArgs args)
{
GlobalClass.Ask = args.Ask;
GlobalClass.Bid = args.Bid;
// I have back ground worker code that updatestables from the global class
}
This all works fine. So long as I do not touch any other buttons on the form UI. As soon as I click a button on the form of the UI... I no longer receive events from my API, the UI application functions normally, but with no data from the API.
Why do events from the UI stop any further events coming from the API event?
Any idea whats going on here? Or suggestions how to design this?
Does the same problem occur if you comment out your code that updates the tables from the global object? and if you comment out the background worker?
It would be a good idea to distinguish if the event stops being fired just after you press some button on the UI, or if it stops being fired only after some line of code you wrote is being executed.
In order to be able to help you, we would need to know how the event on the MT4DDE class is triggered.
If you have the code for this class, posting it would help.
If you don't you may want to use a tool such as Reflector to decompile the assembly into C# and see what the MT4DDE class is doing that might cause it to stop invoking the event.
In addition, if you are doing anything related to background threads, or if you're doing anything unusual with your application's main message loop, it would be a good idea to mention it here.
I have tried to use the invoke command, it works, but after a few events it stops...here is the code isolated:
using MT4API;
namespace WindowsFormsApplication1
{
public delegate void UpdateTextCallback(double ask, double bid);
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
string symbol = "GBPUSD";
MT4DDE dde = new MT4DDE("");
dde.OnQuote += new EventHandler<QuoteEventArgs>(MT_OnQuote);
dde.Connect();
dde.Subscribe(symbol);
}
private void updateTickDisplay(double ask, double bid)
{
textBox1.Text = ask.ToString();
textBox2.Text = bid.ToString();
}
private void button1_Click(object sender, EventArgs e)
{
this.Close();
}
private void MT_OnQuote(object sender, QuoteEventArgs args)
{
BeginInvoke(new UpdateTextCallback(this.updateTickDisplay),
new object[] { args.Ask, args.Bid });
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
textBox3.Text = textBox1.Text;
}
}
}
The only difference from the real code is that I am using a data grid....as opposed to a text field. But it is clear that the UI blocks somehow the new events. It is strange that I get about 5 to 10 events and then it just stops. Strange. Any ideas on a differnet design?

Load previous session values when open new window

There is a MenuItem click event MainMenu_Define_Material which opens a window called Material. I get info from a textbox called txt_density and I save that info in density and return to main window via OK-button having click event Material_btn_OK_Click.
My question is, how I can avoid passing the last session values every time I open the window Material?
I want, once density is set, every time I open Material window I want to see the values of previous session.
private void MainMenu_Define_Material(object sender, RoutedEventArgs e)
{
newWin_material = new Material();
newWin_material.btn_OK.Click += new RoutedEventHandler(Material_btn_OK_Click);
if (density != -1)
{
newWin_material.txt_density.Text = density.ToString();
}
newWin_material.ShowDialog();
}
private void Material_btn_OK_Click(object sender, RoutedEventArgs e)
{
density = System.Convert.ToSingle(newWin_material.txt_density.Text);
newWin_material.Close();
}
Not very clear where that density comes from, but you can insert that field in uour data class what you can hold on data layer or may be like ObjectDataProvider for modelview.
There are a lor of different solutioms our there. The basic idea is:
Define common, shared data storage and keep there alll values you want to share between different windows.
Define a data class. I used here a static class with a static member Desnity.
public static class DataClass
{
public static double Desnsity;
}
After in any window you're able to access that field (read/write), like
DataClass.Density
Hope this is clear.
There is no concept of Session in WPF. You can try creating a static variable to do this or a app config value or pass a parameter via the constructor.
If you are going to new it ( e.g. new Material(); ) then it is going to start with zero information. That is what new does. What is wrong with passing the value in the ctor?
Try
MainWindow
{
private newWin_material = new Material(); // just new it ONCE
// this may need to be in MainWindow ctor.
newWin_material.btn_OK.Click += new RoutedEventHandler(Material_btn_OK_Click);

AutoComplete Texbox error - write to protected memory

I have an autocompleate textbox that looks into a data base. Some times while I'm typing I received the following error.
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
Here is the code
private void tBSearchName_TextChanged(object sender, EventArgs e)
{
try
{
//test length
if (tBSearchName.Text.Length > 3)
{
//prevent db lookups
if (!tBSearchName.Text.ToLower().Contains(oldName) || oldName == String.Empty)
{
//test for a name + first letter of last name
if (Regex.IsMatch(tBSearchName.Text, #"(\w)+\s(\w)+(\.)*"))
{
tBSearchName.AutoCompleteCustomSource = AccessDB.serachByNemberName(tBSearchName.Text);
tBSearchName.AutoCompleteMode = AutoCompleteMode.Suggest;
//prevent db lookups
oldName = tBSearchName.Text.ToLower();
}
}
}
}
catch
{
}
}
My insight is that I should frezz typing into the application while search is done, can some suggest how to do this. Or any other insight on what is happening
It is a bug in Windows Forms's wrapper of autocomplete APIs. Windows Forms does not protect the AutoCompleteCustomSource object from being replaced while it is being enumerated by a background thread created by autocomplete.
Instead of replacing the data store, you can try replace the autocomplete object or use the IAutoCompleteDropDown interface to reset the enumerator.
You can use lock:
private void tBSearchName_TextChanged(object sender, EventArgs e)
{
lock(this) { /* do magic */
}
Do note that it's bad practice to perform long tasks in the event handlers. If the search takes more then 30ms, better use a worker thread.

Providing multiple instances of a form yet processing events one at a time

I need to be able to let multiple instances of the same form be open as my application can be used in different places at once. On the other hand I need to be able to process the operations during the "OK" event one at a time to ensure data is stored safely and not overwritten by another form instance by accident.
I show my form using the .Show() method as I am using a few delegates in it:
private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
bookingForm = new BookingForm(AddMemberBooking, AddUserBooking, CloseBooking);
bookingForm.Show();
}
I have tried to use the mutex to allow only one event of the OK button being pressed happen at a time, i have combined this with a Thread to meet the criteria i need.
When i click on the "OK" button I am given the following error:
Cross-thread operation not valid: Control 'comboBoxDay' accessed from a thread other than the thread it was created on.
This is the code for my booking form class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace Collection
{
//Allows the class to be serialized
[Serializable()]
public delegate void AddMemberBookingMethod(int date, int time, int mNo);
public delegate void AddUserBookingMethod(int date, int time, string fName, string lName, string pCode);
public delegate void CloseBookingFormMethod();
public partial class BookingForm : Form
{
public CloseBookingFormMethod CloseBookingForm;
public AddMemberBookingMethod AddMemberBooking;
public AddUserBookingMethod AddUserBooking;
private Mutex bookingMut = new Mutex();
private Thread thread;
public bool IsUser;
public BookingForm(AddMemberBookingMethod ambm, AddUserBookingMethod aubm, CloseBookingFormMethod cbfm)
{
InitializeComponent();
AddMemberBooking = ambm;
AddUserBooking = aubm;
CloseBookingForm = cbfm;
checkBoxMember.Checked = true;
//Control.CheckForIllegalCrossThreadCalls = false;
}
private void checkBoxUser_CheckedChanged(object sender, EventArgs e)
{
if (checkBoxUser.Checked)
{
IsUser = true;
checkBoxMember.CheckState = CheckState.Unchecked;
textBoxMNo.Enabled = false;
textBoxFName.Enabled = true;
textBoxLName.Enabled = true;
textBoxPCode.Enabled = true;
}
else
{
IsUser = false;
checkBoxMember.CheckState = CheckState.Checked;
textBoxMNo.Enabled = true;
textBoxFName.Enabled = false;
textBoxLName.Enabled = false;
textBoxPCode.Enabled = false;
}
}
private void checkBoxMember_CheckedChanged(object sender, EventArgs e)
{
if (checkBoxMember.Checked)
{
IsUser = false;
checkBoxUser.CheckState = CheckState.Unchecked;
textBoxFName.Enabled = false;
textBoxLName.Enabled = false;
textBoxPCode.Enabled = false;
}
else
{
IsUser = true;
checkBoxUser.CheckState = CheckState.Checked;
textBoxMNo.Enabled = false;
textBoxFName.Enabled = true;
textBoxLName.Enabled = true;
textBoxPCode.Enabled = true;
}
}
private void buttonOK_Click(object sender, EventArgs e)
{
this.thread = new Thread(new ThreadStart(MakeBooking));
this.thread.Name = "bookingThread";
this.thread.Start();
}
private void MakeBooking()
{
this.bookingMut.WaitOne();
int date = this.comboBoxDay.SelectedIndex;
int time = this.comboBoxTime.SelectedIndex;
if (IsUser)
{
string fName = textBoxFName.Text;
string lName = textBoxLName.Text;
string pCode = textBoxPCode.Text;
AddUserBooking(date, time, fName, lName, pCode);
}
else
{
int mNo = int.Parse(textBoxMNo.Text);
AddMemberBooking(date, time, mNo);
}
this.bookingMut.ReleaseMutex();
CloseBookingForm();
}
private void buttonClose_Click(object sender, EventArgs e)
{
CloseBookingForm();
}
}
}
I realise I may not be doing this in the most efficient way but time is a bit of a factor.
I've researched the error and have heard of using delegates and .Invoke() but I'm still not entirely sure how to fix it.
EDIT:
I've found this code snippet when searching for a fix to my problem. I don't understand where/how I would use it.
if(this.InvokeRequired)
{
this.Invoke(new MyEventHandler(this.CreateAForm()));
return;
}
EDIT2:
Seems the guy finally saw sense, by creating the from with the new word it apparently passes the criteria. I wish I'd have known this before trying to reinvent the wheel.
You are getting this exception because your thread is accessing controls. That's not legal, control properties must only ever be accessed from the UI thread. You're okay on the TextBox.Text property, that one happens to be cached. But not ComboBox.SelectedIndex. And closing the form from another thread is going to bomb too.
Your mutex has nothing to do with it, but keep it if you want to prevent threads from overlapping. Using a delegate's Invoke method isn't going to solve it, that just starts a thread as well. You'll need to collect the info that the thread is going to need in a little helper class and pass that as the argument to the Thread.Start() method.
Closing the form is a bit tricky too, the user might well have already closed it while the thread was running. That's going to cause an ObjectDisposed exception. A quick fix is to set the form's Enabled property to false so the user can't close it. You'll need to use the form's Invoke() method to ensure the closing is done on the UI thread.
Last but not least, if these threads don't take a lot of time (a second or so), consider not using threads at all and display a wait cursor instead.
One simple way to do this is to use the overload of the Thread.Start method that accepts an object: Thread.Start Method (Object). In this object you will store all the data/state necessary in order to make the update.
All the code that references the form and its controls needs to be moved into the OK click event method or refactored out to a method that just returns a data object. Then pass this object into the thread start method.
Some pseudo code:
on_click_event()
{
object data=getFormData();
thread.start(data);
}
There are better ways to do this but this is a quick fix for your code.
I think you could simply disable the OK buttons on other open forms to give users a visual cue. Then you shouldn't even have the issue. Provide a callback delegate to something in the application controller which knows which forms are open. Each form can provide a public method to disable the OK button. Disable to OK button on all the other forms.
I'm not really following your code too well. I would think the mutex could be outside of the form code in the first place (i.e. in the delegates that do the actual work), and if it is within a single application, you could just use the lock (object) method to ensure only one thread is executing a given bit of code.
I'd also like to add that a mutex is not going to stop multiple users on different machiens being able to click OK at the same time. I'm not sure if that's what you meant in your question by a form being run in different places.
I think that AddUserBooking and the other delegate should be responsible for ensuring that they are threadsafe and this should not be part of the UI. If they aren't threadsafe, why aren't they? It's relatively easy to make database commit functions each have their own connection to the database during their operations and thread-safety should not be an issue.

Categories

Resources