Reputation: 7414
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
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
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