2

I have a TreeView control, which is populated with items (one for every file in a specific directory), and a Thread which runs in the background, which slowly populates each node in the tree with extra data.

I then need the TreeViewNode to update its Text field, based on the results.

I've tried using BeginInvoke on the worker for each item, after its been processed, but this causes the GUI thread to become unresponsive, due to the shear number of nodes that the background worker is processing. C# - Updating GUI using non-main Thread

public static void InvokeIfRequired(this System.Windows.Forms.Control c,
                                    Action action) {
    if (c.InvokeRequired) {
        c.Invoke((Action)(() => action()));
    }
    else {
        action();
    }
}

I've tried batching up several node updates into a single work item for the GUI, and using BeginInvoke to update several at once, during which the background thread waits on a signal to continue its work, but this also causes the GUI to become unresponsive during the Invoked method (seems like updating TreeViewNode is quite an expensive operation?)

Lastly, I created a queue on the GUI thread, which is populated by the worker thread, and is polled when the Application is idle, which works nicely, but feels like a complete hack.

using System;
using System.Runtime.InteropServices;

namespace System
{
    struct PointAPI
    {
        public Int32 x;
        public Int32 y;
    }

    struct WindowsMessage
    {
        public Int32 hwnd;
        public Int32 message;
        public Int32 wParam;
        public Int32 lParam;
        public Int32 time;
        public PointAPI pt;
    }

    static class IdlePolling
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool PeekMessage(ref WindowsMessage lpMsg,
                                                Int32 hwnd,
                                                Int32 wMsgFilterMin,
                                                Int32 wMsgFilterMax,
                                                Int32 wRemoveMsg);
        static public bool HasEventsPending()
        {
            WindowsMessage msg = new WindowsMessage();
            return PeekMessage(ref msg, 0, 0, 0, 0);
        }
    }
}

...
Application.Idle += mainForm.OnIdle;
...

    public partial class MainWindow : Form
    {
        ...
        public void OnIdle(object sender, EventArgs e)
        {
            while (IdlePolling.HasEventsPending() == false)
            {
                ConsumeGUIUpdateItem();
                Thread.Sleep(50);
            }
        }
    }

So what is the "correct" way of updating lots of gui items, which won't cause the GUI thread to hang in the process.

Community
  • 1
  • 1
russ
  • 21
  • 1
  • 1
    Classical abuse of a UI. The UI is user oriented, a user cannot handle more than a few dozen items so you shouldn't be loading thousands. – H H Jul 04 '11 at 09:39
  • In the past I simply wrote some code to push events for every update into the UI threads. It worked great for 100's of updates per second. – CodingBarfield Jul 04 '11 at 09:48
  • @Henk - perhaps adding the items occurs in bursts, leaving the user plenty of time to interact with the treeView? The strange thing is that the UI can obviously handle the updates because the 'complete hack' works, so why is the performance of BeginInvoke so bad? – Martin James Jul 04 '11 at 09:52
  • The 'hack' works, because it only executes when the message queue for the main form is empty, and it has a Sleep in there. Which means that as soon as the form wants to repaint, the OnIdle function returns after the next item has finished updating, and no more items are updated until Application.OnIdle event is received again. – russ Jul 04 '11 at 10:29
  • @MartinJames I would guess the BeginInvoke performance is bad, as every frame, potentially the background thread could've asked the main thread to do N items of work, and the GUI thread can't keep up with the burden, neglecting the other messages? – russ Jul 04 '11 at 10:38

1 Answers1

1

russ, good attempts there. in summary: i think the fix is to invoke the batch rather than begininvoke(). let me explain:

i had a similar problem when i was adding many items to a data grid in WPF where the worker would begin invoke (or the equivalent in WPF) to the UI thread. i too found that under high invokes the new items were not inserted as efficiently as they should. the asnc invoke represented a high percentage of overhead for this particular setup which was interesting.

in the end, i resorted to a simpler method and merely inserted batches (like you have done), but rather then insert them via async thread marshalling, i instead just did the WPF equivalent to a synchronous Invoke() instead of async BeginInvoke().

as you probably know, each time you begininvoke(), .net places a message in the Windows Message Pump to process the call. however if your app is busy with other messages the insert may take a while. similarly, too many begininvokes() hinders other windows message pump messages from being processed. ultimately, when too many are sent windows may discard excess so your method may never be called. this is true regardless of c++ or .net as you may know.

ultimately, windows can only do one thing at a time in the UI thread, whether it is doing typical GUI stuff or handling your async method.

cheers