ThreadOption.UserInterface not working

Topics: CAB & Smart Client Software Factory
Apr 7, 2007 at 9:47 PM
Edited Apr 7, 2007 at 9:50 PM
I have what I think is a simple use of the CAB EventTopic publish and subscribe, but it is not working properly.
I have one module that starts a new thread which polls for some data. When it arrives it fires a global event topic. In my other module, which contains a number of views that will be updated based on this data arriving, I have an event topic subscription, and it specifies that it wants to be on the UI thread. Should be this simple, I thought... But the handler is actually being called right on the same thread that fired the event. And so my attempts to update the UI end in Exceptions (Cross-thread operation not valid) indicating I can’t do it on that thread... I have log messages that prove this is the case, and I’ve single stepped through the code and it never switches threads...

Any clues would be greatly appreciated.

Here is how I fire the event in the first module:
                        EventArgs<Msg> ev = new EventArgs<Msg>(messageEnvelope);
                        workItem.EventTopics[EventTopicNames.MessageArrived].Fire(
                            this,
                            ev,
                            workItem,
                            PublicationScope.Global);
                        }
Here is how I subscribe to the event topic in my UI Module’s ModuleController class...
        [EventSubscription(EventTopicNames.MessageArrived, ThreadOption.UserInterface)]
 
        public void OnMsgReceived(object sender, EventArgs<Msg> ev)
        {
            Msg theMessage = ev.Data;
            if (theMessage.TransactionCode == 321)
            {
                MessageBox.Show("Got a 321 Dispatch message! Time to process it...");
                try
                {
                    myKeyDispatchInfoView.DisplayText = theMessage.msgText;
                }
                catch (Exception e)
                {
                    Tracer.TraceError("Exception trying to set DisplayText of KeyDispatchInfoView in OnMsgReceived" + e.Message);
                    MessageBox.Show("An Exception occurred:" + e.Message);
                }
            }
        }
Thanks for any help you might provide!
Apr 8, 2007 at 7:18 PM
I have had a similar problem with ThreadOption.UserInterface in the past. I think it comes down to when you add the class that has the event subscription to the WorkItem - i.e. when Dependency Injection takes place. Specifically, which thread is used to add the class to the WorkItem. I have had a quick look at the CAB source code and it appears that ThreadOption.UserInterface just takes affinity with the thread that is used to added the class to the WorkItem . The sync context for that thread is cached and calls to the event subscriber are made on that thread. The assumption must be that event subscribers will be added to a WorkItem on the main/GUI thread.

In our case we were adding a class containing an event subscription to the WorkItem in a callback from an async call to a Web Service (using a 'Smart' Web Reference and a Service Agent).

I considered modifying the CAB code, but in the end I just used ThreadOption.Publisher and manually marshalled onto the main/GUI thread before adding a view to a WorkSpace (I cast the WorkSpace to Control and used BeginInvoke on it).
Apr 9, 2007 at 4:29 PM
Very interesting. If you look at most examples, it is the View's Presenter class that subscribes to these events and specifies to execute on the UI thread, because obviously the Presenter will want to alter the UI based on the event. My sample code was done quickly to just prove the eventTopic subscription got the event and we can alter the view. In the long run, my controller class will get the event, it will ignore those that are not relevant to this module, and for those that are relevant to the views in this module, it will turn around and post its own event, posting only that part of the original event's data that is relevant to the views. Then the Presenters of these views will subscribe (on the UI thread) to this new event, and will make any changes to themselves as deemed necessary.

This second approach is much more in the spirit of the CAB framework, letting the Views subscribe to the events they need to, and the maintainer of the ModuleController just needs to find the relevant messages and fire the prescribed events.

Your assumption, though, doesn't seem to be true, either, though. I added a default constructor to my ModuleController, tracing its creation, and it is indeed created on the UI thread. So why the ThreadTopic.UserInterface option seems to work in the view's Presenter class, but not in a ModuleController, is still a mystery waiting to be solved...
Apr 12, 2007 at 6:19 PM
I've looked into this further, this is what I've found:
When I subscribe to the event explicitly like this, my event handler does indeed get called on the UI thread:
        public override void Run()
        {
            AddServices();
            ExtendMenu();
            ExtendToolStrip();
            AddViews();
            // Explicitly subscribe to this event topic
            WorkItem.EventTopics{EventTopicNames.MessageArrived].AddSubscription(
                this, "OnMsgReceived", WorkItem, ThreadOption.UserInterface);
        }

But when I decorate my event handler like the following, (in ModuleController.cs) this event handler gets called actually on the same thread on which it was fired (which is NOT the UI thread).
        [EventSubscription(EventTopicNames.MessageArrived, ThreadOption.UserInterface)]
        public void OnMsgReceived(object sender, EventArgs<Msg> ev)
        {
            ...
        }

The relevant code I found while debugging (which I could only do when I explicitly subscribe to the event, although someone who knows how could do this by stepping through the CAB code that handles the decorations...)
If SynchronizationContext.Current is null at the time the subscription is being added, then the subscription’s syncContext property will NOT get set, and I believe this is likely the ultimate issue. You can see below the code snippet at subscription time. Below that is a code snippet at Fire execution time, and it clearly will simply call the event handler directly on the current thread if the syncContext property was not set at subscription time.
At Subscription time:
In EventBroker\Subscription.cs Ln 93
         if (threadOption == ThreadOption.UserInterface)
         {
                // If there's a syncronization context (i.e. the WindowsFormsSynchronizationContext 
                // created to marshal back to the thread where a control was initially created 
                // in a particular thread), capture it to marshal back to it through the 
                // context, that basically goes through a Post/Send.
                if (SynchronizationContext.Current != null)
                {
                      syncContext = SynchronizationContext.Current;
                }
         }
At Fire time:
In EventBroker\Subscription.cs Ln 215
    private void CallOnUserInterface(object sender, EventArgs e, List<Exception> exceptions)
    {
          Delegate handler = CreateSubscriptionDelegate();
          if (handler != null)
          {
                if (syncContext != null)
                {
                      syncContext.Send(delegate(object data)
                      {
                            try
                            {
                                  ((Delegate)data).DynamicInvoke(sender, e);
                            }
                            catch (TargetInvocationException ex)
                            {
                                  exceptions.Add(ex.InnerException);
                            }
                      }, handler);
                }
                else
                {
                      try
                      {
                            handler.DynamicInvoke(sender, e);
                      }
                      catch (TargetInvocationException ex)
                      {
                            exceptions.Add(ex.InnerException);
                      }
                }
          }
    }
Apr 13, 2007 at 2:51 PM
SynchronizationContext.Current will be set to a WindowsFormsSynchronizationContext for the current thread if a form or UserConrol has been created. Therefore you must make the call to register the event on the thread that creates the visual components, after at least one Form/Control has been created - or construct your own synchronization context.
You can debug the objectbuilder to watch events being registered via attributes (i.e. step into WorkItem.Items.Add). I guess that in your case the attribute was processed before a Form/Control had been created (it will happen during WorkItemController construction - before Run is called), whereas the explicit call to register the event happens after a Form/Control has been created (it's after AddViews in your example).
Apr 13, 2007 at 4:28 PM
That makes a lot of sense. I did verify that the class is instantiated (and therefore "CAB Processed" on the same thread that creates the UI, so I assume, therefore, that the issue is that it is "CAB processed" before the Shell form gets created. Because the "manual" subscription ocurred in the Run() method, which adds views to workspaces in the Shell, this method will always get called after the Shell form has been created. Programmers just need to watch out for this. At least this thread offers a very good workaround, should others run into this issue, as there is no functionality lost by subscribing manually vs. decorating the method.