1

I have an issue with some of my properties not being updated when I use a Dispatcher from a separate thread. When I check for values in the array, they're all 0's.

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];
    Initialize();
}

Methods:

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

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++)
    {
        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()));
    }
}

INotifyPropertyChanged:

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(string name)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(name));
    }
}

The problem is that after this is ran, each index of YearsCount is set at 0. If I get rid of Task.Run() and Dispatcher, the program freezes up a bit during the lengthy operation, but everything is displayed properly. So, I'm messing up somewhere and failing to properly update the YearsCount property.

EDIT (SOLUTION?):

Huge thanks to Garry Vass, who hanged in there through two of my lengthy questions, Jon Skeet (from the first post), and Shoe. I was able to get it up and running without any issues like this:

XAML

Four years:

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

Four stats (one per year):

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

C# Code

Data Object:

public class MissingReportInfoModel : INotifyPropertyChanged
{
    private string _year;
    public string Year
    {
        get { return _year; }
        set
        {
            if (_year == value)
            {
                return;
            }

            _year = value;
            OnPropertyChanged("Year");
        }
    }

    private int _count;
    public int Count
    {
        get { return _count; }
        set
        {
            if (_count == value)
            {
                return;
            }

            _count = value;
            OnPropertyChanged("Count");
        }
    }

    public MissingReportInfoModel()
    {
        Year = "Not Set";
        Count = 0;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

ViewModel Properties:

private ObservableCollection<MissingReportInfoModel> _yearlyStats;
public ObservableCollection<MissingReportInfoModel> YearlyStats
{
    get { return _yearlyStats; }
    private set
    {
        if (_yearlyStats == value)
        {
            return;
        }

        _yearlyStats = value;
        OnPropertyChanged("YearlyStats");
    }
}

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

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

ViewModel Constructor:

public MissingReportsViewModel()
{
    YearlyStats = new ObservableCollection<MissingReportInfoModel>();
    Initialize();
}

ViewModel Methods:

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

private void SetYears()
{
    for (int i = 0; i < 4; i++)
    {
        var info = new MissingReportInfoModel();
        info.Year = DateTime.Now.AddYears(-i).Year.ToString();
        YearlyStats.Add(info);
    }
}

private void SetCounts()
{
    for (int i = 0; i < 4; i++)
    {
        int n = i;
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
            new Action(() => 
            {
                YearlyStats[n].Count = MissingCollection.Where(item =>
                    item.RepNum.Substring(0, 4).Equals(YearlyStats[n].Year)).ToList().Count();
            }));
    }
}
B.K.
  • 9,982
  • 10
  • 73
  • 105
  • 1
    Your call to OnPropertyChanged for YearCounts is for the array, but not for each ELEMENT in the array. Does it make sense? – Gayot Fow May 03 '14 at 20:29
  • @GarryVass It does, I wonder if I should use objects with `INotifyPropertyChanged` implemented, instead of an integer array? – B.K. May 03 '14 at 20:32
  • 1
    You might try `ObservableCollection` – jamesSampica May 03 '14 at 20:36
  • @Shoe That one works very well, but my worry is the nature of `ObservableCollection` order accuracy. Does it insert new elements in order or is it similar to some of collections where you can't rely on a specific element being at exact index? – B.K. May 03 '14 at 20:39
  • 1
    @GarryVass Shoe's suggestion works very well, but I have to read up on `ObservableCollection` index reliability. If it's not reliable, then I'll just use objects like you've mentioned. – B.K. May 03 '14 at 20:40
  • Show your `XAML` for YEARS and YEARSCOUNT – 123 456 789 0 May 03 '14 at 20:43
  • @lll The XAML is really not an issue, it was the fact that I simply wasn't updating the collection elements, but the collections themselves. Shoe and Garry Vass got it right. I'm using Shoe's suggestion for now, but I'm working towards Garry's for reliability and re-usability purposes. – B.K. May 03 '14 at 20:47
  • @B.K. The XAML is the issue here if you are using `string[] and int[]`. Of course if you use `ObservableCollection` you don't have to worry about notifying your property, i.e., calling `RaisePropertyChanged`. As I watch the comment you raised concerns about `OC`. – 123 456 789 0 May 03 '14 at 20:55
  • guys can we have a clear answer on this so its abundantly clear how to solve the issue? – brumScouse May 03 '14 at 20:58
  • @lll XAML is here: http://stackoverflow.com/questions/23447054/strange-behavior-in-task-run – B.K. May 03 '14 at 21:02
  • @brumScouse Posting an edit right now, check back in a minute. – B.K. May 03 '14 at 21:17
  • @GarryVass Posted an edit with the solution based on your idea. Thanks a ton for your help. If you want to post your suggestion as an answer, I'll gladly accept it. – B.K. May 03 '14 at 21:29

1 Answers1

2

When you are presenting a collection of items, there's a few things to keep in mind, among them are...

  • Keep the relationships under a single object. Your original design used parallel arrays which is more vulnerable to falling out of sync;
  • The collection itself needs a public getter so that the binding engine can find it AND it needs to give change notifications. ObservableCollection of T gives you that;
  • Each property in the element needs to also give change notifications. Having the class inherit from INPC accomplishes that.
  • Changes to properties are automatically marshalled by the WPF binding engine into the correct thread, But changes to an ObservableCollection of T (add, remove) must be marshalled by the View Model.

Here's a representative class for your Missing Reports...

  public class MissingReportInfo : INotifyPropertyChanged
    {
        private string _year;
        public string Year
        {
            [DebuggerStepThrough]
            get { return _year; }
            [DebuggerStepThrough]
            set
            {
                if (value != _year)
                {
                    _year = value;
                    OnPropertyChanged("Year");
                }
            }
        }
        private int _count;
        public int Count
        {
            [DebuggerStepThrough]
            get { return _count; }
            [DebuggerStepThrough]
            set
            {
                if (value != _count)
                {
                    _count = value;
                    OnPropertyChanged("Count");
                }
            }
        }
        #region INotifyPropertyChanged Implementation
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string name)
        {
            var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
        #endregion
    }

Note that it uses 'CompareExchange' on the Interlocked class so that potential race conditions are avoided.

The declaration for the collection would look like this...

public class ViewModel
{
    public ObservableCollection<MissingReportInfo> MissingReportInfos { get; set; } 
    public void Initialize()
    {
        MissingReportInfos = new ObservableCollection<MissingReportInfo>();
    }
}

Finally, the Xaml is using text blocks that anticipate a fixed collection. It's awkward. Instead try one of the collection containers. Here is a sample Items Control set up to present the Missing Reports...

    <ItemsControl ItemsSource="{Binding MissingReportInfos}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="5">
                    <TextBlock Text="{Binding Year}"/>
                    <TextBlock Text=":"/>
                    <Rectangle Width="10"/>
                    <TextBlock Text="{Binding Count, StringFormat='###,###,##0'}" HorizontalAlignment="Left"/>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

Add styling and what-not as appropriate. Now the Xaml doesn't care about the physical dimensions of the collection and more importantly developers who have to maintain the code will be grateful for a clean implementation.

Gayot Fow
  • 8,710
  • 1
  • 35
  • 48
  • 1
    Those are great tips and I'm sure they'll help more people down the road. Thank you so much for giving me a lot of your time today. My Application runs smoothly and I can finally have some rest. :D – B.K. May 04 '14 at 00:23
  • 1
    @B.K. by now we have played all the chords on the wpf piano. Go forth and ROCK! :) – Gayot Fow May 04 '14 at 00:25
  • 1
    Very insightful, and lucid. Cheers chaps. – brumScouse May 04 '14 at 19:43