-2

I'm making my way through threading design patterns and am stuck trying to resolve a small practice project. I get a [InvalidOperationException: 'Cross-thread operation not valid: Control 'txtEventLogger' accessed from a thread other than the thread it was created on.'] when an event in my UI triggered by my object in another thread, calls a method in my UI from the UI event handler, to log some events in a textbox. How can I make this thread safe? Or am I using the wrong design pattern for handling object events when it comes to using threads?

My GUI WinForm has 3 TextBoxes and 1 button:

  • txtAmountOfMessages
  • txtMessageToSend
  • txtEventLogger
  • btnStart

I have a class named Relay, and another class derived of EventArgs CustomEventArgs for event handling.

public class Relay
{
    //CustomEventArgs, derived EventArgs class to hold object index and a string message
    public delegate void OnSend(object sender, CustomEventArgs e);
    public event OnSend Sent;

    public int index;
    public int Index {get => index; set => index = value; }
        
    public void Send(string UserMessage)
    {
        //TODO: Meanwhile trigger event for testing

        //EventArgs class not shown, but it takes:
        //CustomEventArgs Constructor  CustomEventArgs(int index, string message)
        Sent(this, new CustomEventArgs(this.index, UserMassage.Length.ToString()));
    }

}

My WinForm code:

public partial class Form1 : Form
{
    private void btnStart_Click(object sender, EventArgs e)
    {
        // create a couple threads
        for (int i = 1; i < 3; i++)
        {                                               // txtAmountOfMessages = 3
            new Thread(() => CreateRelaysAndSendMessage(Convert.ToInt32(txtAmountOfMessages.Text), txtMessageToSend.Text)).Start();
        }
    }

    private void CreateRelaysAndSendMessage(int AmountOfMessages, string Message)
    {
        List<Relay> relayList = new List<Relay>();

        // Create 5 objects of Relay class, index, and subscribe to ui eventhandler
        for (int i = 0; i <= 4; i++)
        {
            relayList.Add(new Relay());
            relayList[i].Index = i;
            relayList[i].Sent += RelaySent;
        }

        // For each object, call .Send, 3 times (from txtAmountOfMessages value)
        foreach(Relay myRelay in relayList)
        {
            for (int j = 1; j <= AmountOfMessages; j++)
            {
                myRelay.Send(Message);
            }
        }
    }

    private void RelaySent(object sender, CustomEventArgs e)
    {
        // Exception handling error here
        Log("Relay number " + e.Index.ToString() + " sent msg " + e.Message);            
    }

    public void Log(string Message)
    {
        // Exception handling error here
        //System.InvalidOperationException: 'Cross-thread operation not valid: Control 'txtEventLogger' accessed from a thread other than the thread it was created on.'
        txtEventLogger.AppendText(Message + Environment.NewLine);
    }
}
  • What kind of class is the `Relay`? Is it intended to be used as a UI component, like the `BackgroundWorker` or the `System.Timers.Timer`? Or is it intended to be used mainly by the middle-tier? – Theodor Zoulias Nov 24 '20 at 10:58
  • Just a normal class, no relevance in particular. I just want to test event handling between objects in multiple threads and the UI. I just want to be able to read the EventArgs into a textbox. – Charlie Brown Nov 24 '20 at 13:54
  • For the general case you could just use `Invoke` or `BegineInvoke` inside the event handler, to marshal the invocation to the UI thread. You can look [here](https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread "How do I update the GUI from another thread?") for details. – Theodor Zoulias Nov 24 '20 at 14:50

1 Answers1

0

This has a relatively straight forward answer for anyone else looking.

Because you are trying to access controls on a different thread, you simply need to marshall them back by calling BeginInvoke. That's actually quite easy!

Change this :

txtEventLogger.AppendText(Message + Environment.NewLine);

To this :

txtEventLogger.BeginInvoke((Action)(() => txtEventLogger.AppendText(Message + Environment.NewLine));

Begin Invoke simply tells .NET that you want to invoke it on the UI thread (Which you should be doing, and what the error is trying to tell you).

Working with delegates can be a pain but it's part of life if you want to do multi threaded work in Windows Forms. Other options are :

  1. Use the Windows Dispatcher (Search System.Windows.Threading.Dispatcher), although this is essentially the same thing as calling BeginInvoke on your control really.
  2. Use a threading library that has helpers for marshalling calls. Generally these allow you to just decorate methods with an attribute to say "Hey, I want this run on the UI thread please", and it does the rest (More info here https://dotnetcoretutorials.com/2020/12/10/simplifying-multithreaded-scenarios-with-postsharp-threading/)
MindingData
  • 11,924
  • 6
  • 49
  • 68