Search code examples
c#multithreadingexceptionpluginstoolstrip

How to add Control to Control from another thread, without Invoke?


I created an app which uses plugins. Plugin contains ToolStrip which I want to add to main ToolStrip Container Panel (in Form1 class). This is easy container.TopToolStripPanel.Controls.Add(plugin.PluginToolStrip;, but if I want to run plugins code in separated thread, then it's not as easy. (I use multithreading for easy way to unload plugin, I only need to kill plugin thread, and remove ToolStrip from main form)

I disabled CheckForIllegalCrossThreadCalls = false; to allow not-using Invoke void. But when I want to run container.TopToolStripPanel.Controls.Add(plugin.PluginToolStrip); from another Thread then program throws ArgumentException and says that I can't do it.

So, how can I create plugins architecture with possibility to kill plugins threads? (I want to give user possibility of easy way of management plugins)

I decompilled System.Windows.Forms.dll to see where it throws that exception and I saw:

            /// <summary>Adds the specified control to the control collection.</summary>
            /// <param name="value">The <see cref="T:System.Windows.Forms.Control" /> to add to the control collection. </param>
            /// <exception cref="T:System.Exception">The specified control is a top-level control, or a circular control reference would result if this control were added to the control collection. </exception>
            /// <exception cref="T:System.ArgumentException">The object assigned to the <paramref name="value" /> parameter is not a <see cref="T:System.Windows.Forms.Control" />. </exception>
            public virtual void Add(Control value)
            {
                if (value == null)
                {
                    return;
                }
                if (value.GetTopLevel())
                {
                    throw new ArgumentException(SR.GetString("TopLevelControlAdd"));
                }
                if (this.owner.CreateThreadId != value.CreateThreadId)
                {
                    throw new ArgumentException(SR.GetString("AddDifferentThreads")); //here!
                }
                /* [...] */
            }

then I think that if i can change this.owner.CreateThreadId, then I will be able to pass this if (if (this.owner.CreateThreadId != value.CreateThreadId)), and program will not throw exception. On line 6315 i saw this code:

internal int CreateThreadId
        {
            get
            {
                if (this.IsHandleCreated)
                {
                    int num;
                    return SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num);
                }
                return SafeNativeMethods.GetCurrentThreadId();
            }
        }

we have only get, and it's internal :(

What can I do? Do you have any suggestions? Thanks and sorry for my bad English...


Solution

  • I disabled CheckForIllegalCrossThreadCalls = false; to allow not-using Invoke void.

    That solves nothing. The property just enables throwing exceptions when your code does something wrong, but disabling it doesn't fix the underlying issues that the exceptions are there to try to help you avoid.

    The bigger issue is that UI objects have "thread affinity". They are owned by a specific thread, i.e. the thread in which their window handle is created, and if you try to access those objects from any other thread, that access may fail or cause the control to operate incorrectly.

    So, how can I create plugins architecture with possibility to kill plugins threads? (I want to give user possibility of easy way of management plugins)

    Killing threads is dangerous in and of itself. There is no guarantee that you can kill a thread safely, in a way that won't take down the rest of your process or corrupt its data. You may get away with it, even most of the time, but it's not a reliable way to manage things.

    In theory, if you do decide to continue down this path, one option is to go ahead and create the plugin controls in a new thread. Then you have to make sure that thread is an STA thread, and you have to provide that thread with a message loop, by calling Application.Run() in that thread.

    However, you will still run into the problem that your plugin controls will be hosted in a window that is owned by a different thread. This is another area of danger, and may be difficult to get to work correctly, assuming you can do it at all. Having a window owned by one thread, and which is a child of a window owned by a different thread will have its own pitfalls.

    The most reliable way I know of to be able to terminate plugin code safely is to run that code in its own AppDomain. Then you can tear down the AppDomain at will. Since domains can't directly access each other's data, this will avoid the problems that would normally come up aborting threads.

    However, that solution will require some sort of proxying between domains. You won't be able to have the actual user interface part of the object live in the separate domain. Instead, you'll have to set up a system by which the user interacts with some component that your code is in control of, where those interactions translate into some proxied communication with the plugin's implementation.

    It's actually a doable thing. It's likely, since you're dealing with toolbars, that the plugins' user interaction is limited to a few simple controls (like buttons, menus, etc.) and you may be able to design a decent API to allow the necessary cross-domain communication. But you'll have to really want this level of safety, as opposed to just taking the risk that a user may use a buggy plugin that could take down the whole process. That it can be done doesn't mean it'll be worth the effort.