-1

I'm working with TreeView Control in Windows Forms in C#.

Since actual population part of TreeView Control takes a lot of time it freezes the UI. So I'm attempting to do the population using PostMessage Win32 API from a background thread but I found that the Treeview isn't getting inserted with Items.

So I moved the code from background thread to main thread. But then also the Insert Item Code is not working. I got similar code working with TreeView in C++ and trying to do the same thing with C# using interop routines.

I'm not going the usual C# way of treeView1.Nodes.Add("...") because it freezes the UI even if I follow the Delegate method and BackgroundWorker method for populating UI controls from another thread. I'm giving the code I use below. Can some one please help to find the issue with the code.

Also Please note for the TreeView Control I'm using my own simple class derived from TreeView class, where I have overriden the WndProc method to verify the flow of Windows Messages and I can see the messages(TVM_INSERTITEM) are actually getting through but still the item is not getting populated

Also I have got similar interop code working fine from Background Thread for ListView Control but my attempts with TreeView haven't succeeded so far.

Form Class Code

using System;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace UpdateTreeViewFromAnotherThread
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct TVITEM
{
    public uint mask;
    public IntPtr hItem;
    public uint state;
    public uint stateMask;
    public IntPtr pszText;
    public int cchTextMax;
    public int iImage;
    public int iSelectedImage;
    public int cChildren;
    public uint lParam;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct TVINSERTSTRUCT
{
    public IntPtr hParent;
    public IntPtr hInsertAfter;
    public TVITEM item;
}

public enum TreeViewInsert
{
    TVI_ROOT = -0x10000,
}

[Flags]
public enum TreeViewItemMask
{
    TVIF_TEXT = 0x0001,
}


public partial class Form1 : Form
{
    const int TV_FIRST = 0x1100;
    IntPtr tvInsItemPtr;
    TVINSERTSTRUCT tvins;
    IntPtr handleTreeView;

    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);
    public enum TreeViewMessage
    {
        TVM_INSERTITEM = TV_FIRST + 50,        
    }
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        handleTreeView = treeView1.Handle;
        //treeView1.Nodes.Add("hello");
        PopulateTree(handleTreeView);
    }

    public void PopulateTree(IntPtr handle)
    {
        tvins = new TVINSERTSTRUCT();
        tvins.item.mask = (uint)TreeViewItemMask.TVIF_TEXT;

        // Set the text of the item. 
        string productName = "Product";
        string value = productName;
        byte[] buffer = new byte[100];
        buffer = Encoding.Unicode.GetBytes(value + "\0");
        tvins.item.pszText = Marshal.AllocHGlobal(buffer.Length);
        Marshal.Copy(buffer, 0, tvins.item.pszText, buffer.Length);

        tvins.hParent = IntPtr.Zero;
        tvins.hInsertAfter = (IntPtr)(TreeViewInsert.TVI_ROOT);
        tvInsItemPtr = Marshal.AllocHGlobal(Marshal.SizeOf(tvins));
        Marshal.StructureToPtr(tvins, tvInsItemPtr, true);
        PostMessage(treeView1.Handle, (uint)TreeViewMessage.TVM_INSERTITEM, IntPtr.Zero, tvInsItemPtr);
        //SendMessage(treeView1.Handle, (int)TreeViewMessage.TVM_INSERTITEM, 0, tvInsItemPtr);
    }
}
}

MyTreeView Class Code

using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace UpdateTreeViewFromAnotherThread
{
    class MyTreeView:TreeView
    {
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == 0x1132)
            {
                TVINSERTSTRUCT anotherTVInsertStruct;
                anotherTVInsertStruct = (TVINSERTSTRUCT)Marshal.PtrToStructure(m.LParam, typeof(TVINSERTSTRUCT));
                string anotherNodeText = Marshal.PtrToStringAnsi(anotherTVInsertStruct.item.pszText);
            }
            if(m.Msg == 0x113F)
            {
                TVITEM anotherTVItem;
                anotherTVItem = (TVITEM)Marshal.PtrToStructure(m.LParam, typeof(TVITEM));
                string anotherNodeText = Marshal.PtrToStringAnsi(anotherTVItem.pszText);
            }
            base.WndProc(ref m);

            //Trace.WriteLine(m.Msg.ToString() + ", " + m.ToString());
        }
    }
}

Update_1

Prevented NM_CUSTOMDRAW for Treeview using the below code. Thanks to the code atlink.

        protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_REFLECT + WM_NOTIFY:
                {
                    NMHDR nmhdr = (NMHDR)m.GetLParam(typeof(NMHDR));
                    switch ((int)nmhdr.code)
                    {
                        case NM_CUSTOMDRAW:
                            NMTVCUSTOMDRAW nmTvDraw = (NMTVCUSTOMDRAW)m.GetLParam(typeof(NMTVCUSTOMDRAW));
                            switch (nmTvDraw.nmcd.dwDrawStage)
                            {
                                case CDDS_ITEMPREPAINT:
                                    m.Result = (IntPtr)CDRF_DODEFAULT;
                                    break;
                            }
                            Marshal.StructureToPtr(nmTvDraw, m.LParam, false);
                            return;
                    }
                    break;
                }
        }
        base.WndProc(ref m);
    }

So Now If I change my earlier PopulateTree function (note Thread.Sleep()) and its invocation to a background thread as below it will not freeze the UI during the population process

    private void button1_Click(object sender, EventArgs e)
    {
        handleTreeView = treeView1.Handle;
        Thread backgroundThread = new Thread(() => PopulateTree(handleTreeView));
        backgroundThread.Start();
    }

    public void PopulateTree(IntPtr handle)
    {
        for(int i =0; i< 1000; i++)
        { 
            tvins = new TVINSERTSTRUCT();
            tvins.item.mask = (uint)TreeViewItemMask.TVIF_TEXT;

            // Set the text of the item. 
            string productName = "Product_" + i.ToString();
            string value = productName;
            byte[] buffer = new byte[100];
            buffer = Encoding.Unicode.GetBytes(value + "\0");
            tvins.item.pszText = Marshal.AllocHGlobal(buffer.Length);
            Marshal.Copy(buffer, 0, tvins.item.pszText, buffer.Length);

            tvins.hParent = IntPtr.Zero;
            tvins.hInsertAfter = (IntPtr)(TreeViewInsert.TVI_ROOT);
            tvInsItemPtr = Marshal.AllocHGlobal(Marshal.SizeOf(tvins));
            Marshal.StructureToPtr(tvins, tvInsItemPtr, true);
            PostMessage(handle, (uint)TreeViewMessage.TVM_INSERTITEM, IntPtr.Zero, tvInsItemPtr);
            Thread.Sleep(1000);
        }
    }
  • 1
    Does this answer your question? [Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on](https://stackoverflow.com/questions/142003/cross-thread-operation-not-valid-control-accessed-from-a-thread-other-than-the). **Short answer:** even if you could, updating the UI from a worker thread isn't going to make it faster. You might want to look into [`TreeView.BeginUpdate`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.treeview.beginupdate?view=netframework-4.8) –  Mar 13 '20 at 10:54
  • Hi MickyD thanks for the quick response. But my problem is UI responsiveness. Showing wait cursor is not an option. I have managed to get List view control populated by similar code from background thread. So if I put a sleep in the for loop of the thread populating the TreeView control, I can see ListView getting filled up one item by one. I can drag around my window while it is getting populated. So though it says cross thread operation is not valid i have it working for ListView.. and UI doesn't freeze.. So in short it doesn't resolve my issue.. – Ganeshkumar P S Mar 13 '20 at 11:39
  • You can use `Control.BeginInvoke()`. This method uses `RegisterWindowMessage` and `PostMessage` to marshal the call to the Thread where the Control was created. You can run a Task (`Task.Run(() => [Method()])`) and, in a BeginUpdate/EndUpdate section you `BeginInvoke()` the Control. You don't need 90% of all that. There are some notes about this [here](https://stackoverflow.com/a/60571698/7444103) (another language, but all .NET stuff). – Jimi Mar 13 '20 at 14:25
  • _"...But my problem is UI responsiveness"_ - Agreed. That's why you should look into _freezing updates_ via calls to methods such as `BeginUpdate`. Also, you should look into _batched_ updates. i.e. not inserting everything at once. Whilst it is fine to use a worker thread to calculate _what_ items need to be added, you should marshal the batch over to the UI thread to actually _add_ them. This is a common practice. It removes the wait cursor without thrashing your UI pump –  Mar 13 '20 at 15:13
  • Jimi, Micky D thanks for your updates... I believe both of you guys are referring to the same approach. I will try it out – Ganeshkumar P S Mar 14 '20 at 05:08
  • I got a solution at this [link](https://social.msdn.microsoft.com/Forums/en-US/75f23fba-8937-43bf-a351-fb32ae5b46a8/cannot-populate-treeview-control-with-windows-messages-in-csharp?forum=windowsgeneraldevelopmentissues&prof=required). I prevented NM_CUSTOMDRAW as suggested using the code that I have updated in my question. But I will be pursuing the BeginUpdate and BeginInvoke methods as mentioned by Jimi and Micky D as it seems to be the more C# way of doing things.. Need to see whether the UI responsiveness is same as that of the approach Preventing NM_CUSTOMDRAW approach – Ganeshkumar P S Mar 17 '20 at 05:43

1 Answers1

0

Thanks Jimi and MikiD I was able to produce same non-freezing UI behaviour using the BeginUpdate and BeginInvoke approach. I changed my code as below

        private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => PopulateTree());
    }

    private async void PopulateTree()
    {
        for(int i = 0;i< 1000;i++)
        {
            treeView1.BeginInvoke( (MethodInvoker)delegate ()
                {
                    treeView1.BeginUpdate();
                    treeView1.Nodes.Add("Product_" + i.ToString());
                    treeView1.EndUpdate();
                }
            );

            System.Threading.Thread.Sleep(1000);
        }
    }