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() {..}
有帮助吗?

解决方案

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.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top