views:

208

answers:

2

Hi there!

Firstly, I know I should be using proper Threading techniques (Threadpool, BeginInvoke, etc.) to accomplish this, but thats a bit over my head currently and will call for some time to read over material and understand it (if you have any URL references for my scenario, please feel free to post it).

In the interim I am using the backgroundWorker to pull a very large resultset and populate a DatagridView with it. I successfully create a SortableBindingList<TEntities> in my DoWork event and pass that out in the result. And in the RunWorkerCompleted event, I cast and bind that SortableBindingList<TEntities> to my Grid. My 2 main areas of concern are as follows:

1) Access to private variables. I want to pass one of two parameters List<long> into my DoWork event, but run a different query depending on which list was passed to it. I can get around this by declaring a class-level private boolean variable that acts a flag of sorts. This seems silly to ask, but in my DoWork, am I allowed to access that private variable and route the query accordingly? (I've tested this and it does work, without any errors popping up)

private bool SearchEngaged = false;

private void bgw_DoWork(object sender, DoWorkEventArgs e) {
    BackgroundWorker worker = sender as BackgroundWorker;
    e.Result = GetTasks((List<long>)e.Argument, worker, e);
}
SortableBindingList<Task> GetTasks(List<long> argsList, BackgroundWorker worker, DoWorkEventArgs e) {
    SortableBindingList<Task> sbl = null;
    if (worker.CancellationPending) {
        e.Cancel = true;
    }
    else {
        if (SearchEngaged) {
            sbl = DU.GetTasksByKeys(argsList);
        }
        else {
            sbl = DU.GetTasksByDivision(argsList);
        }
    }
    return sbl;
}

2) UI Thread freezes on beginning of RunWorkerCompleted. Ok, I know that my UI is responsive during the DoWork event, 'cos it takes +/- 2seconds to run and return my SortableBindingList<Task> if I don't bind the List to the Grid, but merely populate it. However my UI freezes when I bind that to the Grid, which I am doing in the RunWorkerCompleted event. Keep in mind that my Grid has 4 image columns which I handle in CellFormatting. This process takes an additional 8 seconds to accomplish, during which, my UI is completely non-interactive. Im aware of the cross-thread implications of doing so, but is there any way I can accomplish the Grid population and formatting either in the background or without causing my UI to freeze? RunWorkeCompleted looks like so:

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if (e.Cancelled) {
        lblStatus.Text = "Operation was cancelled";
    }
    else if (e.Error != null) {
        lblStatus.Text = string.Format("Error: {0}", e.Error.Message);
    }
    else {
        SortableBindingList<Task> sblResult = (SortableBindingList<Task>)e.Result;
        dgv.DataSource = sblResult;
        dgv.Enabled = true;
        TimeSpan Duration = DateTime.Now.TimeOfDay - DurationStart;
        lblStatus.Text = string.Format("Displaying {0} {1}", sblResult.Count, "Tasks");
        lblDuration.Visible = true;
        lblDuration.Text = string.Format("(data retrieved in {0} seconds)", Math.Round(Duration.TotalSeconds, 2));
        cmdAsyncCancel.Visible = false;
        tmrProgressUpdate.Stop();
        tmrProgressUpdate.Enabled = false;
        pbStatus.Visible = false;
    }
}

Sorry for the lengthy query, but I will truly appreciate your responses! thank you!

+1  A: 

Your code appears to be doing exactly the right thing.

As for the 8 seconds that it takes for the UI thread to update the screen, there's not much you can do about that. See my answer to this question.

To optimise the UI part, you could try calling SuspendLayout and ResumeLayout on the grid or its containing panel.

You could also look at trying to reduce the amount of processing that is done during the data binding. For example:

  • Calculations done in the grid could be moved into the data model (thereby doing them in the worker thread).
  • If the grid auto-calculates its columns based on the data model, then try hard-coding them instead.
  • EDIT: Page the data in the Business Layer and make the grid only show a small number of rows at a time.
Christian Hayter
Hey christian! thanks for your input! calling SuspendLayout and ResumeLayout doesn't quite resolve my situation. There aren't any calculations as such - more cell formatting. And the only place I can do that is in the Grid's CellFormatting event. Im contemplating 2 workarounds.
Shalan
**ONE:** Run my query as per normal in DoWork, and for each object returned in the List, I can create and populate a DataGridViewRow and pass that into the ProgressChanged event, where I can then add that row to the Grid (dunno if thats worth the additional processing and if its even possible!)
Shalan
**TWO**: I know that this goes against all best practices regarding winforms grids, but under my circumstances, I think it may be worth extending the Grid to facilitate paging. Iv noticed that if I only bring back 100 rows, the Grid "paints" itself in 0.4 seconds. Your thoughts?
Shalan
Re: ONE - Not a good idea. You would be populating a large UI-related mutable object in one thread then passing it to another thread to render. This is a multithreading "code smell" and I would not recommend it.
Christian Hayter
Re: TWO - Good idea. It would make the UI more scalable. Paging itself is never a problem, it's where you perform the paging that you have to watch. Returning 100 rows from the Business Layer = good, returning 100000 rows from the Business Layer then paging it in the Presentation Layer = bad.
Christian Hayter
Yep, Im more inclined to go with option TWO. I agree with you about bringing back too much from BL is bad practice. However, consider this - I fetch and populate a List<TEnties> with 5013 objects in .31 seconds (i can see that clearly if I run my existing code commenting out the line: *dgv.DataSource = sblResult;* yeah - an addition ~7secs to paint the grid)If I now assign the returned sblResult to a private class variable, would that not facilitate sorting and paging more efficiently?
Shalan
I may be way off with this as its just an assumption...If I bring back only 100 rows from the BL, and then sort a column, Im only sorting on those 100 rows on not the entire result set. It may seem heavy and unnecessary to bring back the entire lot, but a smoother UX is preferred over something that may be construed as a good logical design practice. Sorry, Im not knocking your opinion on this...just sharing mine in the hopes of a better understanding and acquiring a compromisable solution.
Shalan
Yes, saving sblResult to a private class variable would be a good compromise in this case. You could do grid-level paging over the whole resultset.
Christian Hayter
If you do paging in the BL, you have to do *everything* yourself. If the user changes the sort order, you have to go all the way back to the BL, re-query the data with the new sort order, then re-fetch the first page of new data.
Christian Hayter
EXACTLY!! +1 2 u! filling and *then* working with a 'local' represntation of data will result in a more user-friendly experience imo. As I figured, which u confirmed in your comment above this - I actually think this would involve more of a processing overhead, not to mention delays from constant transactions over a network with probable latency.
Shalan
additionally, Im using LINQ so returning a List and using Skip() and Take() will work to my advantage
Shalan
+1  A: 

I think the easiest solution for your problem is setting the datasource of your grid in DoWork instead of RunWorkerCompleted using Dispatcher.BeginInvoke which you have mentioned yourself. Something like this:

private bool SearchEngaged = false;

private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    SortableBindingList<Task> sblResult = GetTasks((List<long>)e.Argument, worker, e);

    BeginInvoke((Action<object>)(o => dataGridView1.DataSource = o), sblResult);
}

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled) {
        lblStatus.Text = "Operation was cancelled";
    }
    else if (e.Error != null) {
        lblStatus.Text = string.Format("Error: {0}", e.Error.Message);
    }
    else
    {
        dgv.Enabled = true;
        TimeSpan Duration = DateTime.Now.TimeOfDay - DurationStart;
        lblStatus.Text = string.Format("Displaying {0} {1}", sblResult.Count, "Tasks");
        lblDuration.Visible = true;
        lblDuration.Text = string.Format("(data retrieved in {0} seconds)", Math.Round(Duration.TotalSeconds, 2));
        cmdAsyncCancel.Visible = false;
        tmrProgressUpdate.Stop();
        tmrProgressUpdate.Enabled = false;
        pbStatus.Visible = false;
    }
}

As far as the private variable issue is concerned, I don't think it will be of any problem in your case. In case you are changing it using some UI event, just mark the private field as volatile. The documentation of the volatile keyword can be found here:

http://msdn.microsoft.com/en-us/library/x13ttww7.aspx

Yogesh
Lambda expressions...nice! thanks for the reply yogesh. Although "sblResult" shows as null when I put a breakpoint just after the BeginInvoke and then run the app. Any ideas?"Dispatcher.BeginInvoke" shoud be rewritten as "Dispatcher.CurrentDispatcher.BeginInvoke" (WindowsBase.dll)
Shalan
Grid does not get populated at all. I was mistaken before - I see that sblResult does contain data, but nothing happens in the grid. Is there something I am missing?
Shalan
Sorry, I posted a WPF related solution. Check my post again. Edited.
Yogesh
Hey yogesh. U have assisted me a lot wrt my 1st issue, but Im afraid that the above does not resolve my 2nd issue. this time my entire UI locks up with a waitcursor and I was not getting this before
Shalan