Search code examples
c#multithreadingeventsasync-awaittask

Cross-thread operation error ONLY when event-triggering method is called inside another event


Currently, I have a setup for an event-based logging system which works fine EXCEPT when the method that logs an input (i.e. Logger(string message) ) is called inside a method Port_DataReceived(sender, e) when Port.DataReceived is triggered. When this happens, I get the System.InvalidOperationException error even though I have been using Invoke() .

The method I use to log looks like this.

In the first library:

public class LibraryWithLoggingFunctionInIt
{
    SerialPort Port = new SerialPort(/* Port Parameters */);
    
    public LibraryWithLoggingFunctionInIt()
    {
        Port.DataReceived += Port_DataReceived;
    }

    public async Task DoAThing()
    {
        await Task.Delay(1500); //does a thing.
        Logger("I did a thing!");
    }

    public void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        string dataContent = "I received new data!";

        //Read port to dataContent...

        Logger(dataContent);

        //Do other things...
    }

    #region Logging
    public event EventHandler<LoggerMessageArgs> Log;

    public void Logger(string message)
    {
        Log?.Invoke(this, new LoggerMessageArgs(message));
    }
    #endregion
}

public class LoggerMessageArgs
{
    public string Message { get; }

    public LoggerMessageArgs(string message)
    {
        Message = message;
    }
}

In the main form:

public partial class Form1 : Form
{
    LibraryWithLoggingFunctionInIt Libby = new LibraryWithLoggingFunctionInIt();

    public Form1()
    {
        InitializeComponent();

        Libby.Log += (sender, e) =>
        {
            LogTextBox.AppendText($@"{DateTime.Now:hh\:mm\:ss\.ffff}: {e.Message}{Environment.NewLine}"); //This is where the error occurs, ONLY when triggered in the event.
        }
    }
}

Basically, I can call DoAThing() , which will put "I did a thing!" into the log with zero issues. However, when the Port_DataReceived(sender, e) is triggered by Port receiving new data, I get the error "System.InvalidOperationException: 'Cross-thread operation not valid: Control 'LogTextBox' accessed from a thread other than the thread it was created on.' when I expect it to just log I received new data! .

Assistance would be much appreciated.

(The original code concept I sourced much of this from was by user IV. on this question.)


Solution

  • It will be an issue with accessing the textbox from a different thread. You can mitigate this with a change to your event handler:

    private void appendToLog(string message)
    {
        if (LogTextBox.InvokeRequired)
        {
            Action safeWrite = delegate { appendToLog(message); };
            LogTextBox.Invoke(safeWrite);
        }
        else
            LogTextBox.AppendText(message); 
    }
    
    Libby.Log += (sender, e) =>
    {
        appendToLog($@"{DateTime.Now:hh\:mm\:ss\.ffff}: {e.Message}{Environment.NewLine}"); 
    }
    

    This is covered here (https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-make-thread-safe-calls?view=netdesktop-8.0)

    I suspect that in the windows desktop the default behavior for async code is that the resumption thread is marshaled back to the originating UI thread which is possibly why the awaited call may have worked. To satisfy curiosity you could inspect this using:

    public async Task DoAThing()
    {
        Debug.Writeline($"Initial Thread: {Environment.CurrentManagedThreadId}");
        await Task.Delay(1500); //does a thing.
        Debug.Writeline($"Resume Thread: {Environment.CurrentManagedThreadId}");
        Logger("I did a thing!");
    }