0

I am trying to run a lengthy operation with the results being displayed in the window, without blocking the UI thread. What I have is a View that has a ListView control, which binds to an ObservableCollection in my ViewModel. I also have a few other TextBlock controls that bind to two arrays. Anything that runs before the lengthy operation within the Task gets displayed and anything after does not. Here's my code to help you understand what I mean:

Four years:

<TextBlock TextWrapping="Wrap" Grid.Row="1" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[1]}"/>
    <Run Text=":"/>
</TextBlock>
<TextBlock TextWrapping="Wrap" Grid.Row="2" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[2]}"/>
    <Run Text=":"/>
</TextBlock>
<TextBlock TextWrapping="Wrap" Grid.Row="3" 
           Style="{DynamicResource SectionBodyTextStyle}">
    <Run Text="{Binding Years[3]}"/>
    <Run Text=":"/>
</TextBlock>

Four fields that hold the counts for each year.

<TextBlock Text="{Binding YearCounts[0]}" TextWrapping="Wrap" 
           Grid.Column="1" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[1]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="1" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[2]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="2" Margin="10,0,0,0"/>
<TextBlock Text="{Binding YearCounts[3]}" TextWrapping="Wrap" 
           Grid.Column="1" Grid.Row="3" Margin="10,0,0,0"/>

ListView to hold the information for each record:

<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Report Number" 
                            DisplayMemberBinding="{Binding RepNum}"/>
            <GridViewColumn Header="Employee ID" 
                            DisplayMemberBinding="{Binding EmployeeId}"/>
            <GridViewColumn Header="Date" 
                            DisplayMemberBinding="{Binding Date}"/>
            <GridViewColumn Header="Time" 
                            DisplayMemberBinding="{Binding Time}"/>
        </GridView>
    </ListView.View>
</ListView>

So far, nothing out of ordinary. However, here's my code related to them.

Properties:

    private string[] _years;
    public string[] Years
    {
        get { return _years; }
        private set
        {
            if (_years == value)
            {
                return;
            }

            _years = value;
            OnPropertyChanged("Years");
        }
    }

    private int[] _yearCounts;
    public int[] YearCounts
    {
        get { return _yearCounts; }
        private set
        {
            if (_yearCounts == value)
            {
                return;
            }

            _yearCounts = value;
            OnPropertyChanged("YearCounts");
        }
    }

    private ObservableCollection<RecordModel> _missingCollection;
    public ObservableCollection<RecordModel> MissingCollection
    {
        get { return _missingCollection; }
        private set
        {
            if (_missingCollection == value)
            {
                return;
            }

            _missingCollection = value;
            OnPropertyChanged("MissingCollection");
        }
    }

Constructor:

public MissingReportsViewModel()
{
    YearCounts = new int[4];
    Years = new string[4];

    Task.Run(() =>
    {
        SetYears();
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

Methods from this ViewModel:

private void SetYears()
{
    for (int i = 0; i < 4; i++)
    {
        Years[i] = DateTime.Now.AddYears(-i).Year.ToString();
    }
}

private void SetYearCounts()
{
    for (int i = 0; i < 4; i++)
    {
        YearCounts[i] =  MissingCollection.Where(item => item.RepNum.Substring(0, 4).Equals(Years[i]))
                                          .ToList().Count();

    }
}

And there's also method from the access worker, but the code is rather lengthy. It basically connects to an Access database and gets some data.

So, my problem is that if I place any methods before MissingCollection = new AccessWorker().GetMissingReports(); portion within Task.Run() or outside of it, they will get displayed on the UI. However, if I place anything after that portion, it won't get displayed in the UI. Doesn't matter if the following method is within Task.Run or not, same result. I've checked and method yield proper values, they just never make it to the UI. I simply don't understand how these two can yield such different results:

// First -  Years get displayed.
// Second - After a little while data gets displayed
// Third - Counts never get displayed.
Task.Run(() =>
{
    SetYears();
    MissingCollection = new AccessWorker().GetMissingReports();
    SetYearCounts();
});


// First -  After a while, data gets displayed.
// Second - Years and counts do not get displayed.
Task.Run(() =>
{
    MissingCollection = new AccessWorker().GetMissingReports();
    SetYears();
    SetYearCounts();
});

I'm obviously doing something wrong, but I can't figure out what. I've tried invoking into the UI thread, but that didn't do anything.

EDIT:

When I try to invoke an update into the UI thread for my YearsCount array bindings, I get an odd out of range exception. Here's the code:

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

private void SetYearCounts()
{
    for (int i = 0; i < 4; i++)
    {
        Application.Current.Dispatcher.BeginInvoke(
            DispatcherPriority.Background, 
            new Action(() => YearCounts[i] =  MissingCollection.Where(
                    item => item.RepNum.Substring(0, 4).Equals(Years[i])).ToList().Count()));
    }
}

When I step through it, it'll go through each index of YearCounts[i], jump out of the SetYearCounts() back to Task.Run(), then jump back into the SetYearsCounts() and use the last i value, which is 4 in Years[i], which, obviously, throws the out of range exception.

None of this happens when I run the code without Task.Run(). It just freezes for UI until the operation is finished.

If I do this:

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
            new Action(() => SetYearCounts()));
    });
}

...and write out each value as it's being assigned in the Debug window:

Debug.WriteLine(YearCounts[i]);

...it'll write outputs there, but not on the UI window. I am thinking it has to do with the fact that arrays only report on their own change and not the change of the items themselves. I can see that in the debug window when only the initial initializations of the arrays get reported, but no the change to the items. However, what is odd is the fact that anything prior to the observable collection gets updated and anything after doesn't. It has probably to do with the view looking at its data context when observable collection calling a change.

Per request, here's GetMissingReports() declarative portion:

public ObservableCollection<RecordModel> GetMissingReports() {..}
B.K.
  • 9,982
  • 10
  • 73
  • 105
  • Just to check, you're mutating your ViewModel in a non-UI thread... and presumably you're binding straight to the ViewModel in the UI? That sounds like a bad idea. Maybe I'm missing something. – Jon Skeet May 03 '14 at 16:36
  • @JonSkeet I'm not exactly sure what you mean. I am fairly new to MVVM. What would be a better way? I'm basically just trying not to freeze the program. If I get rid of `Task.Run()` the program freezes for about 15 seconds, which is annoying for the user. – B.K. May 03 '14 at 16:38
  • Would you be happy for the UI not to freeze, but for all the results to come at once when the long-running operation has finished? That's *relatively* simple. It's harder to update the UI as you go. I'm not an expert on that side of things, but I suspect you should look into more WPF threading details. – Jon Skeet May 03 '14 at 16:45
  • @JonSkeet Yeah, I'd be happy for the results to come in at once. Doesn't really matter, as long as nothing gets frozen and the information is displayed. – B.K. May 03 '14 at 16:51
  • Okay. And can I check which version of C# you're using? This will be easier with C# 5 using async/await. – Jon Skeet May 03 '14 at 16:54
  • @JonSkeet VS2013, C# 5, .NET 4.5.1 – B.K. May 03 '14 at 16:58
  • For the most part, data binding will automatically marshal the updates to the UI thread (see e.g. http://stackoverflow.com/questions/2553333/wpf-databinding-thread-safety). However, if you suspect that it does not do so in your case, you can use Control.Dispatcher.Invoke() to manually marshal the updates. – Rytmis May 03 '14 at 17:01
  • @Rytmis: Gosh - I didn't know the part about property changes automatically being marshaled. That's handy - thanks! – Jon Skeet May 03 '14 at 17:02
  • Usually Dispatcher.Invoke is something I only ever need to do when explicitly manipulating the UI. :) – Rytmis May 03 '14 at 17:04
  • @Rytmis I have no direct access to the control, though, from the view model, and `Application.Current.Dispatcher.Invoke()` didn't do anything. I've no idea why things don't get updated in the second scenario (at the very bottom of my original post). My observable collection has no problems, but the other two properties, if they come after it, do not get updated. If put before the observable collection, they do.... but I can't do it for the count, since I need the collection to get the counts. – B.K. May 03 '14 at 17:05
  • I don't suppose there's any chance that an exception occurs during the execution of your task and it causes the behavior you're seeing? It's a bit of a long shot, but on .NET 4.5 an unobserved task exception won't bring down the application like it used to, so it is possible. – Rytmis May 03 '14 at 17:14
  • @Rytmis You know... you might have a point. Whenever I would run it under invoke, I would get an out or range exception. But then the results would still be displayed in the debugger and no exception would be thrown if I ran it plainly without invoking or task.run(). I'm going to try to hunt it down and report back. But then again, that behavior was only seen on the YearCounts(), not the Year() method... and if I put Year() after the observablecollection assignment, it doesn't get displayed. So, there's probably an exception somewhere in access code, but I don't see it at all? Hmm.. – B.K. May 03 '14 at 17:17
  • @Rytmis This is driving me insane. So, there are no unhandled exceptions in my code except an instance where I attempt to invoke an action into the UI thread. Please read my update where I explain it further. – B.K. May 03 '14 at 18:28
  • @B.K. just the declaration of GetMissingReports is fine, you don't need to include the whole body for us to see it's not an evaluation issue. You can remove the body of the method. – Gayot Fow May 03 '14 at 19:50

1 Answers1

1

For the part about the subscript out of range error, you can create a small WPF app that only does this...

    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 10; i++)
        {
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
                new Action(() => Console.WriteLine(i.ToString())));
        }
    }

and here's what you get...

10 10 10 10 10 10 10 10 10 10

This is caused by inadequate closure of the delegate. To close it off, write a small WPF app that only does this...

    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 10; i++)
        {
            int i1 = i;
            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
                new Action(() => Console.WriteLine(i1.ToString())));
        }
    }

and observe that the results are...

0 1 2 3 4 5 6 7 8 9

The difference is 'closure'. Your Action delegate takes place inside a loop AND is accessing the loop control variable. Using closure will make the exception go away.

For the synchronization part of the question, and based upon what you wrote, it looks like a race condition. If you change this block of code...

private void Initialize()
{
    SetYears();
    Task.Run(() =>
    {
        MissingCollection = new AccessWorker().GetMissingReports();
        SetYearCounts();
    });
}

to this...

    private void OtherInitialize()
    {
        SetYears();
        Task task = new Task(() => { MissingCollection = new AccessWorker().GetMissingReports(); });
        task.ContinueWith((result) => { SetYearCounts(); });
        task.Start();
    }

...then your 'Years' will synch up as expected, and not create the race condition that was occurring. You'll still need to dispatch on the UI thread however.

Gayot Fow
  • 8,710
  • 1
  • 35
  • 48
  • Unfortunately I get the same out of range exception whenever I try to dispatch into the UI thread: `Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Background, new Action(() => YearCounts[i] = MissingCollection.Where(item => item.RepNum.Substring(0, 4).Equals(Years[i])).ToList().Count()));` For some reason it runs Initialize() twice... – B.K. May 03 '14 at 19:16
  • ...or rather, the constructor and everything that follows. – B.K. May 03 '14 at 19:24
  • @B.K. That's a closure/evaluation issue. Different problem from Task.Run synch'ing up. – Gayot Fow May 03 '14 at 19:26
  • Even on the initial run? – B.K. May 03 '14 at 19:28
  • edit your question to show the declaration of GetMissingReports and close off your Action delegate also :) – Gayot Fow May 03 '14 at 19:30
  • I'm not sure what you mean by closing off the Action delegate. I'll post the GetMissingReports, but it doesn't do anything extraordinary. – B.K. May 03 '14 at 19:32
  • @B.K. answer amended to include closure explanation – Gayot Fow May 03 '14 at 19:45
  • Garry, I've added your code and the exception does go away... but my YearsCount array values never get updated: `int n = i; Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => YearCounts[n] = MissingCollection.Where(item => item.RepNum.Substring(0, 4).Equals(Years[n])).ToList().Count()));` – B.K. May 03 '14 at 19:50
  • That's an INPC issue! It's not really fair you know, please see http://meta.stackexchange.com/questions/188625/etiquette-for-russian-doll-questions – Gayot Fow May 03 '14 at 19:56
  • Sorry, Garry, if I knew where the issue stemmed up from to begin with I'd be more clear with my question. That's why I posted as much information as I had. This has been driving me insane for two days and I have a presentation on this due to the regional offices on the 8th... – B.K. May 03 '14 at 19:57
  • Raise another question about the INPC issue and ping me. Your questions about Task.Run and closure have been addressed! Please read http://meta.stackexchange.com/questions/188625/etiquette-for-russian-doll-questions – Gayot Fow May 03 '14 at 20:00
  • How do I ping you? I'll post the new question right away. – B.K. May 03 '14 at 20:01
  • Leave a comment with my tag on it. But as soon as you post it, a ZILLION people will jump in because it's a massively common topic. Ask about 'Binding to value types and INPC' or such :) Somebody may flag it as a dup :) – Gayot Fow May 03 '14 at 20:03
  • I posted a comment there and here's a link, just in case: http://stackoverflow.com/questions/23449301/inpc-issue-between-view-and-viewmodel – B.K. May 03 '14 at 20:12