2

In a Windows Forms project I have a handler for a button that opens a file in Notepad for editing. Once notepad closes I call a function RefreshTextBox() to parse the text file and update a TextBox based on a value. Here is the method that opens Notepad and calls the refresh method once its closed:

private void button_Click(object sender, EventArgs e)
    {
            Process p = new Process
            {
                EnableRaisingEvents = true,
                StartInfo =
                {
                    FileName = "NOTEPAD.EXE",
                    Arguments = _path,
                    WindowStyle = ProcessWindowStyle.Maximized,
                    CreateNoWindow = false
                }
            };

            p.Exited += (a, b) =>
            {
                RefreshTextBox();
                p.Dispose();
            };

            p.Start();
    }

And code to refresh the textbox:

private void RefreshTextBox()
    {
        using (StreamReader reader = File.OpenText(_appSettingsPath))
        {
            string text = reader.ReadToEnd();

            // Code to parse text looking for value...

            // InvalidOperationException thrown here:
            textBox.Text = reader.Value.ToString();
        }
    }

This throws an Exception for trying to update the Control from a thread other than the one it was created on. I'm having trouble understanding why though. I'm not doing this in a new task or backgroundworker or anything like that. Obviously notepad is running in another thread, but the refresh method isn't called until after it's process has exited.

Edit: I should add that this error throws up a Fatal Exception popup when debugging in Visual Studio (as an Admin). It doesn't show the popup when running the application on its own, either the exception is silently swallowed or it doesn't occur then.

Valuator
  • 3,262
  • 2
  • 29
  • 53
  • 2
    The `Exited` event is presumably raised in a separate thread - which is what I'd expect, as otherwise in many cases you'd never see it. (Imagine a console app which doesn't have any kind of "idling" like the Windows Forms thread.) – Jon Skeet Sep 12 '17 at 18:48
  • Here's a post on how to update the UI from another thread that should help you solve your problem. https://stackoverflow.com/questions/661561/how-to-update-the-gui-from-another-thread-in-c – Sam Marion Sep 12 '17 at 18:51

3 Answers3

6

As per documentation if Process SynchronizingObject is not set it will execute exited event in system threadpool to avoid this and run that event handler in UI thread you need to set SynchronizingObject to Form Instance

When SynchronizingObject is null, methods that handle the Exited event are called on a thread from the system thread pool. For more information about system thread pools, see ThreadPool.

If you set

p.SynchronizingObject = WindowsFormName;

Then it will run in same thread or it will execute in a system threadpool thread which will cause crossthread exception.

MSDN Reference

VMAtm
  • 27,943
  • 17
  • 79
  • 125
sumeet kumar
  • 2,628
  • 1
  • 16
  • 24
0
    private void button_Click(object sender, EventArgs e)
    {
        Process p = new Process
        {

            EnableRaisingEvents = true,
            StartInfo =
            {
                FileName = "NOTEPAD.EXE",
                Arguments = _path,
                WindowStyle = ProcessWindowStyle.Maximized,
                CreateNoWindow = false
            }
        };
        //p.SynchronizingObject = this;
        p.Exited += (a, b) =>
        {
            RefreshTextBox();
            p.Dispose();
        };

        p.Start();
    }
    private void RefreshTextBox()
    {
        using (StreamReader reader = File.OpenText(_appSettingsPath))
        {
            string text = reader.ReadToEnd();

            // Code to parse text looking for value...

            //textBox.Text = text; // reader.Value.ToString();
            threadSafeControlUpdate(textBox, text);
        }
    }
    public delegate void updateUIfunc(Control c, object v);
    public void threadSafeControlUpdate(Control c, object v)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new updateUIfunc(threadSafeControlUpdate), c, v);
            return;
        }
        if (c is TextBox && v is string)
        {
            c.Text = (string)v;
        }
    }
wdomains
  • 86
  • 5
  • You can use the p.SynchronizingObject to force it in a thread compatible with normal UI updating as indicated by sumeet kumar - but if you wanted it to run in a separate thread, you can use a delegate to update the control from another thread. I use a generic threadSafeControlUpdate that determines the type of control and the type of data being passed to update. I only copied the textBox code, but my normal threadSafeControlUpdate() handles pictureBox, DataGridView, etc... – wdomains Sep 12 '17 at 21:19
0

I would recommend capturing the synchronization context and posting the RefreshTextBox call onto it. Something like:

 private void button_Click(object sender, EventArgs e)
{
var _synchronizationContext = WindowsFormsSynchronizationContext.Current;
            Process p = new Process
            {
                EnableRaisingEvents = true,
                StartInfo =
                {
                    FileName = "NOTEPAD.EXE",
                    Arguments = _path,
                    WindowStyle = ProcessWindowStyle.Maximized,
                    CreateNoWindow = false
                }
            };

            p.Exited += (a, b) =>
            {
                _synchronizationContext.Post(_=> RefreshTextBox(), null);
                p.Dispose();
            };

            p.Start();
    }
Dhejo
  • 71
  • 5