Search code examples
c#.netmultithreadingwinformsbackgroundworker

BackgroundWorker to fill a DataGridView


EDIT: Solved using this: http://reedcopsey.com/2011/11/28/launching-a-wpf-window-in-a-separate-thread-part-1/

In my project (.net/windows forms) I'm filling a DataGridView with a large DataTable. Filling can take up to 20 seconds, so I'd like an animated loading window. This animation freezes if the thread is busy, so I'll have to use a new thread either for the window or for filling the DataGridView.

I've tried using the BackgroundWorker to show the form, but it'll be a blank white window in the correct shape.

I've also tried using the BackgroundWorker to fill the DataGridView, but it'll throw an error saying the DataGridView is being accessed by a different thread than the one it has been created for. Since the DataGridView is being created in the designer class, I can't just create it in the new thread - plus that solution doesn't sound very elegant.

What's the best way for me to show a working animated form while filling the DataGridView?

Edit: The answer didn't solve the issue for me, so I've tried to break the code down to something that can be presented here. I didn't do it before because it didn't really seem relevant enough to work through some 1k lines of code. Might be something missing or some remnants from previous experiments in the code presented here. Please ignore bad naming of function, that's a legacy thing I'll fix once I got it working. Parts of the code are ancient.

I've made it run without errors, but the frmLoading still isn't animated (it is if I keep it alive while the working thread isn't busy, though).

namespace a
{
    public partial class frmMain : DockContent, IPlugin
    {
        //...
        private delegate void SafeCallDelegate(DataTable dt);
        private Thread thread1 = null;
        private frmLoading frmLoading = new frmLoading();


        public frmMain()
        {
            //...
        }
        //...

        private void FillDataGrid(DataTable dt)
        {
            if(this.InvokeRequired)
            {
                var d = new SafeCallDelegate(FillDataGrid);
                Invoke(d, new object[] { dt });
            }
            else
            {
                //...
                DataGridFiller(dt);
            }
        }

        private void DataGridFiller(DataTable dt)
        {
            BindingSource dataSource = new BindingSource(dt, null);
            //...
            dgvData.DataSource = dataSource;
            //...
            frmLoading.Hide();
        }

        private void btnGetData_Click(object sender, EventArgs e)
        {
            DataTable dt = [...];

            // Wenn Daten vorhanden sind, dann anzeigen
            if (dt != null)
            {
                //...

                frmLoading.Show();
                thread1 = new Thread(() => FillDataGrid(dt));
                thread1.Start();
            }
        }
    }
}

Solution

  • The second approach is the correct one: use a BackgroundWorker to do any work that will freeze the UI if done in the main thread. About the exception you're getting, that's because the call you're making isn't thread-safe (Because the control was created on a different thread that the one whos calling its methods). Plase take a look at this link to MSDN to understand how to make this kind of call between threads, using backgroundWorker's event-driven model.

    From the link:

    There are two ways to safely call a Windows Forms control from a thread that didn't create that control. You can use the System.Windows.Forms.Control.Invoke method to call a delegate created in the main thread, which in turn calls the control. Or, you can implement a System.ComponentModel.BackgroundWorker, which uses an event-driven model to separate work done in the background thread from reporting on the results.

    Take a look at the second example which demonstrates this technique using the backgroundWorker

    EDIT

    From your comments I get that the real problem here is size. The problem is that the thread that owns the DataGridView control, is the thread that is displaying it, so no matter how, if you load all the data at once it will freeze for the time it takes this thread to draw all that data on the screen. Fortunately you're not the first that had this problem, and microsoft got you covered this time. This is, I think, a great example of what the XY problem is, the real problem is that you want to load a huge dataset (X) and the answer on how to do that is here, but instead you asked how to display a loading icon while filling your datagrid whithout the UI freezing (Y), which was your attempted solution.

    This was from a technical perspective but, from an UI/UX perspective I would like you to think about the actuall use of the form. Do you really need all that data loaded every time you visit this section? If the answer is no, maybe you could think about implementing some pagination (example here). Also 200 colums means horizontal scroll even for an ultrawide monitor. I can't really figure out the user case for you to need all that information on a list/table view at once. I think that maybe implemeting a Master-Detail kind of interface could be more useful.

    EDIT 2.0

    I think that you can try to create a whole new Form window in another thread, place it over your datagrid and paint the loading animation there. Meanwhile in the main window you can draw the grid without its painting making the loading animation freeze (the other thread is taking care of the drawing). You may want to hold a reference to the thread and kill it, or maybe better try to hold a reference to the From and close it more gracefully.

    I've never done this but, I think I recall from some legacy application doing something like that, and it was ugly.