Search code examples
c#wpfinotifypropertychangedvalue-type

INotifyPropertyChanged issue, property is not being updated from Dispatcher


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

Solution

  • 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.