Johnathon Sullinger
Johnathon Sullinger

Reputation: 7414

INotifyPropertyChanged strange NullRefereneException

I have a base class that implements the INotifyPropertyChanged that all of my view models inherit from:

public class BaseChangeNotify : INotifyPropertyChanged
{
    private bool isDirty;

    public BaseChangeNotify()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public bool IsDirty
    {
        get
        {
            return this.isDirty;
        }

        set
        {
            this.isDirty = value;
            this.OnPropertyChanged();
        }
    }

    public virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        // Perform the IsDirty check so we don't get stuck in a infinite loop.
        if (propertyName != "IsDirty")
        {
            this.IsDirty = true; // Each time a property value is changed, we set the dirty bool.
        }

        if (this.PropertyChanged != null)
        {
            // Invoke the event handlers attached by other objects.
            try
            {
                Application.Current.Dispatcher.Invoke(() =>
                    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)));
            }
            catch (Exception exception)
            {
                throw exception;
            }
        }
    }
}

I have a primary view model, that instances children view models that wrap a model from the database. I register as a listener to the child view models PropertyChanged event so I can make the primary view model "dirty" when a child is changed. The problem however is that when the child is changed, I get a null reference exception associated with the parent view model.

Primary view model (simplified):

public class DiaryDescriptionViewModel : BaseViewModel, IDataErrorInfo
{
    private Diary diary;

    private ObservableCollection<DiaryDescriptionDetailsViewModel> diaryDescriptions;

    private DiaryDescriptionDetailsViewModel selectedDiaryDescription;

    private List<SectionViewModel> projectSections;

    public DiaryDescriptionViewModel()
    {
    }

    public DiaryDescriptionViewModel(Diary diary, UserViewModel user) : base(user)
    {
        this.diary = diary;

        // Restore any previously saved descriptions.
        var diaryRepository = new DiaryRepository();
        List<DiaryDescription> descriptions = diaryRepository.GetDiaryDescriptionsByDiaryId(diary.DiaryId);

        // Fetch sections for selected project.
        var projectSections = new List<Section>();
        projectSections = diaryRepository.GetSectionsByProjectId(diary.ProjectId);

        // Convert the Section model into a view model.
        this.projectSections = new List<SectionViewModel>(
            (from section in projectSections
             select new SectionViewModel(section))
                .ToList());

        foreach (var projectSection in this.projectSections)
        {
            // We want to set ourself to Dirty if any child View Model becomes dirty.
            projectSection.PropertyChanged += (sender, args) => this.IsDirty = true;
        }

        // Reconstruct our descriptions
        this.DiaryDescriptions = new ObservableCollection<DiaryDescriptionDetailsViewModel>();
        foreach (DiaryDescription description in descriptions)
        {
            SectionViewModel section =
                this.projectSections.FirstOrDefault(s => s.Items.Any(i => i.BidItemId == description.BidItemId));
            BidItem item = section.Items.FirstOrDefault(i => i.BidItemId == description.BidItemId);

            var details = new DiaryDescriptionDetailsViewModel(description, section, item);
            // Commenting this out resolves the NULL Reference Exception.
            details.PropertyChanged += (sender, args) => this.IsDirty = true;

            this.diaryDescriptions.Add(details);
        }

        this.IsDirty = false;
    }

    public ObservableCollection<DiaryDescriptionDetailsViewModel> DiaryDescriptions
    {
        get
        {
            return this.diaryDescriptions;
        }

        set
        {
            this.diaryDescriptions = value;
            this.OnPropertyChanged();
        }
    }

    public DiaryDescriptionDetailsViewModel SelectedDiaryDescription
    {
        get
        {
            return this.selectedDiaryDescription;
        }

        set
        {
            this.selectedDiaryDescription = value;

            if (value != null)
            {
                // If the description contains a biditem DiaryId, then we go fetch the section and biditem
                // associated with the diary description.
                if (value.BidItemId > 0)
                {
                    SectionViewModel sectionViewModel = this.ProjectSections.FirstOrDefault(
                        section => section.Items.FirstOrDefault(item => item.BidItemId == value.BidItemId) != null);

                    if (sectionViewModel != null)
                    {
                        BidItem bidItem = sectionViewModel.Items.FirstOrDefault(item => item.BidItemId == value.BidItemId);

                        this.selectedDiaryDescription.Section = sectionViewModel;
                        this.selectedDiaryDescription.BidItem = bidItem;
                    }
                }

                this.selectedDiaryDescription.IsDirty = false;
            }

            this.OnPropertyChanged();
            this.IsDirty = false;
        }
    }

    public List<SectionViewModel> ProjectSections
    {
        get
        {
            return this.projectSections;
        }

        set
        {
            this.projectSections = value;
            this.OnPropertyChanged();
        }
    }

The child view model:

public class DiaryDescriptionDetailsViewModel : BaseChangeNotify
{
    private readonly DiaryDescription diaryDescription;

    private SectionViewModel section;

    private BidItem bidItem;

    public DiaryDescriptionDetailsViewModel(DiaryDescription description)
    {
        this.diaryDescription = description;

        // If we have a valid biditem identifier (greater than 0) than we need to go and 
        // fetch the item and it's associated funding section.
        if (description.BidItemId > 0)
        {
            var repository = new DiaryRepository();
            this.section = new SectionViewModel(repository.GetSectionByBidItemId(description.BidItemId));
            this.bidItem = repository.GetBidItemById(description.BidItemId);
        }

        this.IsDirty = false;
    }

    public DiaryDescriptionDetailsViewModel(DiaryDescription description, SectionViewModel section, BidItem item)
    {
        this.diaryDescription = description;

        if (description.BidItemId > 0)
        {
            this.section = section;
            this.bidItem = item;
        }

        this.IsDirty = false;
    }

    public int Id
    {
        get
        {
            return this.diaryDescription.DiaryDescriptionId;
        }
    }

    public int DiaryId
    {
        get
        {
            return this.diaryDescription.DiaryId;
        }
    }

    public DiaryDescription Description
    {
        get
        {
            return this.diaryDescription;
        }
    }

    public int BidItemId
    {
        get
        {
            return this.diaryDescription.BidItemId;
        }
    }

    public BidItem BidItem
    {
        get
        {
            return this.bidItem;
        }

        set
        {
            this.bidItem = value;
            this.diaryDescription.BidItemId = value.BidItemId;
            this.OnPropertyChanged();
        }
    }

    public SectionViewModel Section
    {
        get
        {
            return this.section;
        }

        set
        {
            this.section = value;
            this.OnPropertyChanged();
        }
    }
}

So within my unit test, I use the following code:

var diaryRepository = new DiaryRepository();
Diary diary = diaryRepository.GetDiaryById(DiaryId);
var diaryDescriptionViewModel = new DiaryDescriptionViewModel(diary, new UserViewModel());

// Act
diaryDescriptionViewModel.SelectedDiaryDescription =
    diaryDescriptionViewModel.DiaryDescriptions.FirstOrDefault(
        desc => desc.Id == DiaryDescriptionId);

This is the stack trace:

Test Name:  DeleteDiaryDescriptionsById
Test FullName:  UnitTests.ViewModels.DiaryDescriptionViewModelTests.DeleteDiaryDescriptionsById
Test Source:    c:\Users\UnitTests\ViewModels\DiaryDescriptionViewModelTests.cs : line 103
Test Outcome:   Failed
Test Duration:  0:00:02.678712

Result Message: 
Test method Pen.UnitTests.ViewModels.DiaryDescriptionViewModelTests.DeleteDiaryDescriptionsById threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
Result StackTrace:  
at Pen.ViewModels.BaseChangeNotify.OnPropertyChanged(String propertyName) in c:\Users\ViewModels\BaseChangeNotify.cs:line 70
   at ViewModels.BaseChangeNotify.set_IsDirty(Boolean value) in c:\Users\ViewModels\BaseChangeNotify.cs:line 43
   at ViewModels.BaseChangeNotify.OnPropertyChanged(String propertyName) in c:\Users\ViewModels\BaseChangeNotify.cs:line 57
   at ViewModels.DiaryDescriptionDetailsViewModel.set_Section(SectionViewModel value) in c:\Users\ViewModels\DiaryDescriptionDetailsViewModel.cs:line 158
   at ViewModels.DiaryDescriptionViewModel.set_SelectedDiaryDescription(DiaryDescriptionDetailsViewModel value) in c:\Users\ViewModels\DiaryDescriptionViewModel.cs:line 163
   at UnitTests.ViewModels.DiaryDescriptionViewModelTests.DeleteDiaryDescriptionsById() in c:\Users\UnitTests\ViewModels\DiaryDescriptionViewModelTests.cs:line 112

It looks like it is telling me that the object associated with the IsDirty is null, which isn't true. I verified through the debugger that it exists and uncommenting out the DiaryDetailDescriptionViewModel.PropertyChanged event register it works fine. Am I doing this wrong?

Upvotes: 1

Views: 578

Answers (2)

Johnathon Sullinger
Johnathon Sullinger

Reputation: 7414

Thanks to Henk and GazTheDestroyer I was able to resolve this with the following change to my BaseChangeNotify class. The Application.Current object will always be null when ran from a Unit Test, which is what caused the NullReferenceException.

public virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
    // Perform the IsDirty check so we don't get stuck in a infinite loop.
    if (propertyName != "IsDirty")
    {
        this.IsDirty = true; // Each time a property value is changed, we set the dirty bool.
    }

    if (this.PropertyChanged != null)
    {
        // Invoke the event handlers attached by other objects.
        try
        {
            // When unit testing, this will always be null.
            if (Application.Current != null)
            {
                Application.Current.Dispatcher.Invoke(() =>
                    this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)));
            }
            else
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        catch (Exception exception)
        {
            throw exception;
        }
    }
}

Upvotes: 1

GazTheDestroyer
GazTheDestroyer

Reputation: 21261

Application.Current is null when run from unit tests.

You will need to abstract it away behind some interface if you want to schedule stuff to the dispatcher in unit tests, or inject the dispatcher itself.

Upvotes: 3

Related Questions