views:

253

answers:

2

The worry

Is it bad practice to create controls/forms on threads other than the main thread in WinForms? I thought the rule was just that one thread couldn't change something on a control created on another thread?

The scenario

I have an application with some worker threads. Those worker threads may each display their own instance of a form. All things that happen to that form stay within that thread, no other thread will be trying to do anything to that form instance. The form has some Telerik controls, and since upgrading them to a new version, I get the type of error described here:

System.Reflection.TargetInvocationException was unhandled
  Message="Exception has been thrown by the target of an invocation."
  Source="mscorlib"
  StackTrace:
       at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
       at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
       at System.Delegate.DynamicInvokeImpl(Object[] args)
       at System.Windows.Forms.Control.InvokeMarshaledCallbackDo(ThreadMethodEntry tme)
       at System.Windows.Forms.Control.InvokeMarshaledCallbackHelper(Object obj)
       at System.Threading.ExecutionContext.runTryCode(Object userData)
       at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Windows.Forms.Control.InvokeMarshaledCallback(ThreadMethodEntry tme)
       at System.Windows.Forms.Control.InvokeMarshaledCallbacks()
       at System.Windows.Forms.Control.WndProc(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
       at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
       at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
       at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
       at TelerikControlsTest.Program.Main() in C:\Data Files\Projects\Test Projects\TelerikControlsTest\TelerikControlsTest\Program.cs:line 18
       at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: System.InvalidOperationException
       Message="Object is currently in use elsewhere."
       Source="System.Drawing"
       StackTrace:
            at System.Drawing.Image.get_FrameDimensionsList()
            at System.Drawing.ImageAnimator.CanAnimate(Image image)
            at Telerik.WinControls.Primitives.ImagePrimitive.get_Image()
            at Telerik.WinControls.Primitives.ImagePrimitive.GetImage()
            at Telerik.WinControls.Primitives.ImagePrimitive.get_PreferredImageSize()
            at Telerik.WinControls.Primitives.ImagePrimitive.MeasureOverride(SizeF availableSize)
            at Telerik.WinControls.RadElement.MeasureCore(SizeF availableSize)
            at Telerik.WinControls.RadElement.Measure(SizeF availableSize)
            at Telerik.WinControls.Layouts.ContextLayoutManager.UpdateLayout()
       InnerException:

The question

Is it acceptable to create form instances on threads other than the main application thread, or do I have to marshall all calls back to that thread? This used to work until I upgraded the Telerik controls, so either they have a new bug now, or I was doing it wrong all along.

Please help!

A: 

You need to call Application.Run() (or one of it's overloads) on each thread that you try to display a form on. Other than that, you should be fine as long as you don't try to change things across threads.

Noteworthy, is that if you use the Application.Run(Form) overload, then when the form is closed, execution continues after the function call.

Matthew Scharley
+2  A: 

From the stack trace, it looks like the Telerik controls are sharing a single System.Drawing.Image instance, possibly through some global (static) variable. Because you instantiate controls on different threads, the image ends up being accessed from multiple threads at once which is not supported. We have a similar problem with some legacy code (non-GUI) which uses global variables.

To answer your question: yes, what you are doing is acceptable but such a use case is rare enough that the vendor of the 3rdparty controls may decide not to support it. The easiest fix for them (or you if you have the source) is to locate the global variables and make them thread-local like this:

[ThreadStatic]
static int foo;

This will ensure that each thread gets its own "global" variables, isolating them from each other.

Wim Coenen
Ahh. I don't have the source unfortunately, so it looks like I'm just going to have to change my app so that all forms are created on the same thread. Oh well...
Neil Barnwell