Updating the Panel UI from a worker thread

How do I call a Panel function from a Timer thread to update UI elements?

I am communicating with an external web service that returns a list of data items and may be very slow (minutes to hours) to complete. When it does complete, I want to update several fields in the Panel for my C# RhinoCommon Plugin using Eto.Forms under Windows.

I’ve searched the forum and read Rhino - Event Watchers (rhino3d.com) but this issue continues to elude me. Frustratingly, while using the debugger sometimes the GUI updates will start to work, but most of the time they do not. It is unclear to me what changes to cause this intermittent behavior.

My thread is required to poll the server for results, and it is structured like this:

using Timer = System.Timers.Timer;

        protected class PollTimer
        {
            private Int32 m_curJob;
            private MyPanel m_panel = null;

            public PollTimer(MyPanel panel, Int32 ms)
            {
                m_panel = panel;
                Timer t = new Timer(ms);
                t.AutoReset = true;
                t.Elapsed += new ElapsedEventHandler(CheckJob);
                t.Start();
            }

            private async void CheckJob(Object source, ElapsedEventArgs e)
            {
                await Task.Run(() =>
                {
                    var result = MyPlugin.Instance.m_RemoteServer.query(m_curJob);
                    if (!result.completed) return;

                    // The Eto.Forms way to update the UI (updates all jobs)
                    Action action = new Action(m_panel.OnJobItemChanged);
                    Eto.Forms.Application.Instance.Invoke(action);

                    // Using InvokeOnUiThread (updates current job only)
                    uiOnJobComplete complete = new uiOnJobComplete(m_panel.JobIsCompleted);
                    RhinoApp.InvokeOnUiThread(complete, result.id);

                    // Using Invoke() like EventWatcher suggests, but m_Label has no .Dispatch() method
                    var updateJobProgress = new Action<Int32>(jobid => m_panel.JobIsCompleted(jobid));
                    // In this call, RhinoApp.InvokeRequired is "true" so no update is performed.
                    // Also, expanding 'this' in the debugger reports Eto.Forms.UIThreadAccessException
                    updateJobProgress.Invoke(result.id);
                    //updateJobProgress.BeginInvoke(result.id, null, null);    // same result

                    RhinoApp.WriteLine("Job has completed. Terminating polling thread for job {0}", result.id);
                });
            }
        }

The debug message prints as I expect, and when running in the debugger I can step through the individual assignment statements of the UI controls, which complete without error. However, the Rhino UI does not change (usually). I’ve most recently confirmed this with the InvokeOnUIThread() method.

However, in the process of confirming that .BeginInvoke() behaves the same as .Invoke() in the debugger, the controls are now updating, and after restarting the application .BeginInvoke() continues to work. I need to revisit this later but can anyone clarify what is happening or what the correct thing to do here is? I started this process by using .BeginInvoke() by itself and it was not working.

Hi, without going deep into it, here are some problems I see:

Eventhandlers will run on another thread out of the box. You can and should remove async and Task.Run. All it will do is to spawn more threads as required.

“BeginInvoke” will spawn a new worker thread, whereas “Invoke” forces an execution on the UI thread (=Main).

If you create too many threads/tasks and you run them without debugging under a high rate you can simply cause the system not to output anything, because it simply is not fast enough to execute. Debugging such a problem will slow down the execution and make it disappear

If your plan is to invoke on the UI thread, then do it only once! All you doing is to force the system to run code on a the UI thread. It doesn’t matter how many instructions you pass in. But again, make sure you can execute it in that interval

Isn’t it possible for you to eleminate the timer, and provide a callback to when the server is done?

2 Likes

Thanks Tom, that wasn’t it but it led me to resolve the issue. Let me address a couple of your comments:

Regarding all the different Invoke() calls, my intent was to show what I have tried. I only want one working call.

Regarding “too many threads”, I only have the single Timer thread. It triggers every 5 seconds. I use Task.run because I don’t want to hang the GUI thread while making network connections, but this does not run on that thread so I can remove it.

My simplified code now looks like this:

private void CheckJob(Object source, ElapsedEventArgs e)
{
    if(m_busy) return;
    // There are two if(integer) checks, and one find_item_in_listbox check.
    // With the exception of the network call, this function should take microseconds
    m_busy = true;
    var result = MyPlugin.Instance.m_RemoteServer.query(m_curJob);
    m_busy = false;
    if (!result.completed) return;

    uiOnJobComplete complete = new uiOnJobComplete(m_panel.JobIsCompleted);
    RhinoApp.InvokeOnUiThread(complete, result.id);
    RhinoApp.WriteLine("Job has completed. Terminating polling thread for job {0}", result.id);
}

For a bit of added clarity, this type is defined in MyPanel:

public delegate void uiOnJobComplete(Int32 jobid);

This brings me back to my original state - inside m_panel.JobIsCompleted(), the value RhinoApp.InvokeRequired is false, and my GUI update code does not throw any errors. That code looks like:

            m_jobItemProgress.Value = 100;
            m_jobItemProgress.Indeterminate = false;
            m_jobItemMessages.Text = "job complete";

However the actual GUI display does not change. Further, if I expand the this parameter in the debugger, it reports: + ID 'this.ID' threw an exception of type 'System.ObjectDisposedException' string {System.ObjectDisposedException}

That said, the m_jobItemMessages variable can be expanded, and it appears to be set to some default value. I believe this is because I made the PollTimer a static variable (so threads don’t get out of control) and when the document changed I didn’t update the m_panel member, so it continued to reference the empty document loaded at startup. Fixing this fixed my problem.

So, dumb mistake on my part but posting the resolution in case it becomes useful for anyone else trying to troubleshoot something similar.

Regarding, “provide a callback when the server is done” - I had to think about this but I think you mean to modify my query function to accept a callback parameter. That would probably be a good idea. I’m new to C# and its UI paradigms so I had not considered this. C# seems to be taking a lot of ideas from Javascript but I’ve had issues with using features introduced after “.NET Framework 4.8”. I will try to think “more like JS” and less like C/C++.

[Update: No, the server (HTTP) doesn’t notify me when the result is ready so I have to have a polling thread. I’ll keep an eye out for when I can use callbacks but my architecture currently employs method overrides to manage the responses.]

Thanks again.