Search code examples
c#begininvoke

Is this the correct way to call Dispatcher.BeginInvoke/lambda in a loop over an array or list of data items


I inherited some code which used BeginInkoke to add tabs to a TabControl which looked like this:

foreach (DitaNestedContent content in root.Content)
{
    CrlList.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action<TabControl>((tabControl) =>
    {
        TabItem aTab = new TabItem();
        if (content.Paths != null)
        {
            PublicationsListUserControl crlTree = new PublicationsListUserControl(content.Path, filename);
            crlTree.MinWidth = 5;
            aTab.Content = crlTree;
        }

        aTab.Header = content.Name;
        tabControl.Items.Add(aTab);
}), CrlList);

}

This worked until I rebuilt the project after which the correct number of tabs were still created but each one contained the last tab's content (only). I reasoned that timings had changed and the previous code was working merely by accident and that the first BeginInvoke was now only being started after the loop had completed and that the content was therefore equal to the last value by the time it ran.

So I decided to rewrite the code but I was astonished at what finally seemed to work:

List<String> contentPaths = new List<string>();
foreach (DitaNestedContent content in root.Content)
{
    contentPaths.Add(String.Copy(content.Path));
}

for (Int32 i = 0; i < root.Content.Count; ++i)
{
    CrlList.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action<TabControl>((tabControl) =>
    {
        if (i >= root.Content.Count) { i = 0; } 

        TabItem aTab = new TabItem();
        if (contentPaths[i] != null)
        {
            String contentPath = contentPaths[i];
            PublicationsListUserControl crlTree = new PublicationsListUserControl(contentPath, filename);
            crlTree.MinWidth = 5;
            aTab.Content = crlTree;
        }

        aTab.Header = root.Content[i].Name;
        tabControl.Items.Add(aTab);

        ++i;

    }), CrlList);
}

Basically instead of using the current content to invoke the PublicationsListUserControl constructor, I use i inside the lambda to recalculate which root.Content I should be using.

I would have thought (and the guy who wrote the code before me obviously thought) that the values of the variables used would have been calculated and stored for use by the lambda when it was created, not when BeginInvoke began working.

Is this the correct to reliably use BeginInvoke with a lambda in a loop? Or am I way off the mark?


UPDATE

foreach variables can only be captured by C# 5.0 see here:

http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/ http://csharp.2000things.com/2014/09/19/1186-capturing-a-foreach-iteration-variable-in-a-lambda-expression/


Solution

  • What may be happening here is that you've moved down a compiler version from the codes author, prior to C#5 the variable defined in the foreach body (Your content variable) was defined outside of the loop and took the last applicable value, after C#5 that variable is defined inside the loop and is captured appropriately in lambdas.

    http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx