我有一个实现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'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();
}
}
}
所以在我的单元测试中,我使用以下代码:
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:UsersUnitTestsViewModelsDiaryDescriptionViewModelTests.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:UsersViewModelsBaseChangeNotify.cs:line 70
at ViewModels.BaseChangeNotify.set_IsDirty(Boolean value) in c:UsersViewModelsBaseChangeNotify.cs:line 43
at ViewModels.BaseChangeNotify.OnPropertyChanged(String propertyName) in c:UsersViewModelsBaseChangeNotify.cs:line 57
at ViewModels.DiaryDescriptionDetailsViewModel.set_Section(SectionViewModel value) in c:UsersViewModelsDiaryDescriptionDetailsViewModel.cs:line 158
at ViewModels.DiaryDescriptionViewModel.set_SelectedDiaryDescription(DiaryDescriptionDetailsViewModel value) in c:UsersViewModelsDiaryDescriptionViewModel.cs:line 163
at UnitTests.ViewModels.DiaryDescriptionViewModelTests.DeleteDiaryDescriptionsById() in c:UsersUnitTestsViewModelsDiaryDescriptionViewModelTests.cs:line 112
它看起来像是在告诉我与IsDirty相关联的对象是空的,这不是真的。我通过调试器验证它存在并取消注释DiaryDetailDescriptionViewModel。PropertyChanged事件注册它工作正常。我做错了吗?
Application.Current
在运行单元测试时为null。
如果你想在单元测试中调度调度程序,或者注入调度程序本身,你需要将它抽象到某个接口后面。
多亏了Henk
和GazTheDestroyer
,我能够通过以下更改我的BaseChangeNotify类来解决这个问题。应用程序。当从单元测试中运行时,当前对象将始终为空,这就是导致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;
}
}
}