Search code examples
c#winforms

How can two forms on different threads access each other's controls


I searched for a long time to find access methods from parent form to child or vice versa. But they don't work when my forms are on different threads. I need two forms not covering the other all the time so I create the child form on the other thread. Here are my simplified codes. Anyway to modify them? Thanks in advance!

namespace WinFormsApp1
{
    public class FormA : Form
    {
        public static Thread th1;
        FormB FB = new FormB();

        public Button A1;
        public FormA()
        {
            A1 = new Button();
            A1.Click += new EventHandler(A1Click);
            this.Controls.Add(this.A1);

            Thread th1 = new Thread(this.OpenFormB);
            th1.Start();
        }
        void A1Click(Object sender, EventArgs e)
        {
            FB.B1Text("B1");
        }
        void OpenFormB()
        {
            FB.ShowDialog();    //FB.ShowDialog(this); reports cross-thread error
        }
        void A1Text(string text1)
        {
            if (this.A1.InvokeRequired)
            {
                Del1 STR1 = delegate { A1Text(text1); };
                this.Invoke(STR1, text1);
            }
            else
            {
                this.A1.Text = text1;
            }
        }
        delegate void Del1(string text1);

        public class FormB : Form
        {
            public Button B1;
            public FormB()
            {
                B1 = new Button();
                B1.Click += new EventHandler(B1Click);
                this.Controls.Add(this.B1);
            }
            void B1Click(Object sender, EventArgs e)
            {
                FormA FA = (FormA)this.Owner;   //'Owner' is not passed here because of the cross-thread error so the next command fails
                FA.A1Text("A1");
            }
            public void B1Text(string text1)
            {
                if (this.B1.InvokeRequired)
                {
                    Del1 STR1 = delegate { B1Text(text1); };
                    this.Invoke(STR1, text1);
                }
                else
                {
                    this.B1.Text = text1;
                }
            }
        }
    }
}

correct my codes to realize my purpose


Solution

  • This answer is divided in two parts:

    • With multithreading: I kept multithreading as it might be required for your project on a greater scale
    • Without multithreading: Something very similar to what you asked for can be done without multithreading in the first place, avoiding cross-thread access exceptions.

    With multithreading

    As you mentionned in your comment, FB.ShowDialog(this); does not work and causes a "Can't be accessed from another thread" error.

    This is because, when passing Form A as the owner here, the Owner property of Form B gets changed in Form B's thread. But according to the reference source, changing this property calls AddOwnedForm on the new owner, which effectively means that a Form A method is called on Form B's thread.

    An easy workaround is to have a Form A reference in Form B, and to pass it from Form A to Form B when constructing it.

    Here is the code I came up with:

    public partial class FormA : Form
    {
        public static Thread th1;
        FormB FB = null;
    
        public Button A1;
        public FormA()
        {
            InitializeComponent();
    
            A1 = new Button();
            A1.Click += new EventHandler(A1Click);
            this.Controls.Add(this.A1);
    
            FB = new FormB(this);
    
            Thread th1 = new Thread(this.OpenFormB);
            th1.Start();
        }
    
        void A1Click(Object sender, EventArgs e)
        {
            FB.B1Text("B1");
        }
    
        void OpenFormB()
        {
            FB.ShowDialog();
        }
    
        public void A1Text(string text1)
        {
            if (this.A1.InvokeRequired)
            {
                Del1 STR1 = delegate { A1Text(text1); };
                this.Invoke(STR1, text1);
            }
            else
            {
                this.A1.Text = text1;
            }
        }
        delegate void Del1(string text1);
    }
    
    public partial class FormB : Form
    {
        public Button B1;
        
        FormA FA = null;
    
        public FormB(FormA owner)
        {
            InitializeComponent();
    
            B1 = new Button();
            B1.Click += new EventHandler(B1Click);
            this.Controls.Add(this.B1);
    
            this.FA = owner;
        }
    
        void B1Click(Object sender, EventArgs e)
        {
            FA.A1Text("A1");
        }
    
        public void B1Text(string text1)
        {
            if (this.B1.InvokeRequired)
            {
                Del1 STR1 = delegate { B1Text(text1); };
                this.Invoke(STR1, text1);
            }
            else
            {
                this.B1.Text = text1;
            }
        }
        delegate void Del1(string text1);
    }
    

    Do note that this is pretty hacky, we effectively just create an alternate owner.

    Without multithreading

    You don't need multithreading to have two forms existing at the same time.

    The real problem here is that you use FB.ShowDialog(); to show Form B. However, as the documentation for this method says, when this method is called, the code following it is not executed until after the dialog box is closed.

    Which I suppose is why you wanted to have it on another thread, so it could run along with Form A.

    But actually, you can use the Show method instead to show Form B in a non-blocking way.

    This effectively lets you have two forms at the same time, even though they are on the same thread.

    Additionally, Show has an overload to take an IWin32Window as an owner (and the Form class does implement IWin32Window)

    So, you can actually show Form B the following way:

    FB.Show(this);
    

    Even if cross-threading wasn't a problem, it wouldn't have worked because you called ShowDialog without an owner passed as a parameter, which is why you get a crash when trying to use this.Owner in Form B, since it has default value null.

    But as you mentioned in your comments, FB.ShowDialog(this); crashes because Form A isn't on the same thread as Form B.

    Which is why using Show is the best solution here to avoid overcomplicating your code.

    Here is the full code I came up with:

    public partial class FormA : Form
    {
        FormB FB = new FormB();
    
        public Button A1;
        public FormA()
        {
            InitializeComponent();
    
            A1 = new Button();
            A1.Click += new EventHandler(A1Click);
            this.Controls.Add(this.A1);
    
            FB.Show(this);
        }
    
        void A1Click(Object sender, EventArgs e)
        {
            FB.B1Text("B1");
        }
    
        public void A1Text(string text1)
        {
            this.A1.Text = text1;
        }
    }
    
    public partial class FormB : Form
    {
        public Button B1;
    
        public FormB()
        {
            InitializeComponent();
    
            B1 = new Button();
            B1.Click += new EventHandler(B1Click);
            this.Controls.Add(this.B1);
        }
    
        void B1Click(Object sender, EventArgs e)
        {
            FormA FA = (FormA)this.Owner;
            FA.A1Text("A1");
        }
    
        public void B1Text(string text1)
        {
            this.B1.Text = text1;
        }
    }
    

    However, do note that having both forms on the same thread means that when one form is blocked due to some work it is doing, it'll also block the other form.