У меня есть базовый класс, который реализует INotifyPropertyChanged, который наследует все мои модели просмотра:
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;
}
}
}
}
У меня есть модель первичного представления, что экземпляры детей рассматривают модели, которые переносят модель из базы данных. Я регистрируюсь в качестве слушателя для представления объекта PropertyChanged для дочерних элементов, чтобы я мог сделать модель основного вида "грязной", когда ребенок изменен. Однако проблема заключается в том, что, когда ребенок изменен, я получаю исключение нулевой ссылки, связанное с моделью родительского представления.
Модель основного вида (упрощенная):
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();
}
}
Модель детского вида:
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 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();
}
}
}
Поэтому в рамках моего модульного теста я использую следующий код:
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);
Это трассировка стека:
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
Похоже, он говорит мне, что объект, связанный с IsDirty, имеет значение null, что неверно. Я проверил через отладчик, что он существует, и раскомментирует регистр событий DiaryDetailDescriptionViewModel.PropertyChanged, он отлично работает. Я делаю это неправильно?
Application.Current
имеет значение null при запуске от модульных тестов.
Вам нужно будет отвлечь его за некоторым интерфейсом, если вы хотите запланировать материал диспетчеру в модульных тестах или ввести сам диспетчер.
Благодаря Henk
и GazTheDestroyer
я смог решить эту проблему со следующим изменением моего класса BaseChangeNotify. Объект Application.Current всегда будет иметь значение NULL при запуске от Unit Test, что и вызвало исключение 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;
}
}
}
Application.Current
будет нулевым.