WPF中ViewModel列表无法重置至初始状态的问题排查
在ViewModel中维护了两份同类型的列表:一份是绑定到Model的属性,另一份是ViewModel自身的属性,目的是把后者作为初始加载的列表,方便随时执行“重置”操作将Model列表恢复到原始状态。但现在遇到的问题是,修改Model列表中的属性时,原本要保留原始状态的副本里的对应属性也会被修改。虽然用new初始化了副本,但m_originalSubmissionQuestions的初始状态始终无法保留,看起来像是浅拷贝导致的,但不清楚原因。
SubmissionModel
public class SubmissionModel : ModelBase { public int SubmissionId { get; set; } private bool m_noBuildingPlacedInService; public bool NoBuildingPlacedInService { get { return m_noBuildingPlacedInService; } set { if (m_noBuildingPlacedInService == value) return; m_noBuildingPlacedInService = value; RaisePropertyChanged(); } } private bool m_beginPeriodInFollowingYear; public bool BeginPeriodInFollowingYear { get { return m_beginPeriodInFollowingYear; } set { if (m_beginPeriodInFollowingYear == value) return; m_beginPeriodInFollowingYear = value; RaisePropertyChanged(); } } public List<SubmissionSectionModel> Sections { get; } = new List<SubmissionSectionModel>(); }
SubmissionSectionModel
public class SubmissionSectionModel : ModelBase { public bool ShowSectionHeader { get; set; } public string SectionHeader { get; set; } public List<SubmissionQuestionModel> Questions { get; } = new List<SubmissionQuestionModel>(); }
SubmissionQuestionModel
public class SubmissionQuestionModel : ModelBase { public int AnswerId { get; set; } public int SubmissionFk { get; set; } public short QuestionFk { get; set; } public List<SubmissionAnswerModel> Answers { get; } = new List<SubmissionAnswerModel>(); private bool m_revisionRequired; public bool RevisionRequired { get { return m_revisionRequired; } set { if (m_revisionRequired == value) return; m_revisionRequired = value; RaisePropertyChanged(); } } }
SubmissionDetailsViewModel
public class SubmissionDetailsViewModel : ViewModelBase { private readonly IEventAggregator m_eventAggregator; private readonly IAOCData m_aocData; private readonly IUserData m_userData; private ShowControlEvent m_showControlEvent; private Guid m_submissionGuid; private readonly int m_userId; private bool? m_originalIsNoBuildingsInService = null; private bool? m_originalIsOneBuildingInService = null; public SubmissionDetailsViewModel (IEventAggregator eventAggregator, IAOCData aocData, IUserData userData) { m_eventAggregator = eventAggregator; m_aocData = aocData; m_userData = userData; Setup_EventAggregator_EventHandlers(); CurrentViewState = Visibility.Collapsed; } private void Setup_EventAggregator_EventHandlers() { //event raisers m_showControlEvent = new ShowControlEvent(); //event handlers m_eventAggregator.EventHandler<ShowControlEvent>(e => { if (e.ShowControlType == Enumerations.ShowControlTypes.AOCDetail) { if (!Guid.TryParse(e.Key, out Guid _id)) { m_showControlEvent.ShowControlType = Enumerations.ShowControlTypes.AOCSearch; m_eventAggregator.RaiseEvent(m_showControlEvent); MessageBox.Show("Uknown submission id"); return; } //scroll to top and display CurrentViewState = Visibility.Visible; //data is already loaded - no reason to do it again if (_id == m_submissionGuid) { return; } //clear all prior data SubmissionModel = null; DetailLoadingVisibilty = Visibility.Visible; //set the ID m_submissionGuid = _id; //load the data LoadDataAsync(); } else if (e.ShowControlType == Enumerations.ShowControlTypes.AOCSearch) { CurrentViewState = Visibility.Collapsed; } }); } private SubmissionModel m_submissionModel; public SubmissionModel SubmissionModel { get => m_submissionModel; set { if (m_submissionModel == value) { return; } m_submissionModel = value; base.RaisePropertyChanged(); } } public void HandleInServiceChange() { AnnualOwnerCertificationSubmissionSectionModel _questionSection = SubmissionModel.Sections.FirstOrDefault(); foreach (var _question in _questionSection.Questions) { _question.RevisionRequired = true; } } private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(SubmissionModel.NoBuildingPlacedInService) || e.PropertyName == nameof(SubmissionModel.BeginPeriodInFollowingYear)) { HandleInServiceChange(); } } private ObservableCollection<AnnualOwnerCertificationSubmissionQuestionModel> m_originalSubmissionQuestions; public ObservableCollection<AnnualOwnerCertificationSubmissionQuestionModel> OriginalSubmissionQuestions { get { return m_originalSubmissionQuestions; } set { if (m_originalSubmissionQuestions == value) return; m_originalSubmissionQuestions = value; base.RaisePropertyChanged(); } } private async void LoadDataAsync() { //clear everything SubmissionModel = null; //start with getting the submission SubmissionModel _submission; _submission = await m_aocData.GetSubmission(m_submissionGuid); SubmissionModel = _submission; SubmissionModel.PropertyChanged += Model_PropertyChanged; SubmissionSectionModel _questionSection = SubmissionModel.Sections.FirstOrDefault(); m_originalSubmissionQuestions = new ObservableCollection<SubmissionQuestionModel>(_questionSection.Questions); DetailLoadingVisibilty = Visibility.Collapsed; } }
问题原因
你创建的确实是浅拷贝。虽然用new ObservableCollection<SubmissionQuestionModel>(_questionSection.Questions)初始化了新的集合,但集合里的每个SubmissionQuestionModel对象都是引用类型,新集合只是复制了这些对象的引用,并没有创建对象本身的副本。所以当你修改Model列表中对象的属性(比如RevisionRequired)时,副本集合里的引用指向的是同一个对象,属性自然也会跟着变化。
解决方法
要保留原始状态,必须创建深拷贝——也就是不仅复制集合,还要复制集合里的每个对象,以及对象内部的引用类型属性(比如Answers列表)。
方法1:给每个Model实现克隆方法
给SubmissionQuestionModel和相关Model添加Clone方法,手动复制所有属性:
public class SubmissionQuestionModel : ModelBase { // 原有属性... public SubmissionQuestionModel Clone() { var cloned = new SubmissionQuestionModel { AnswerId = this.AnswerId, SubmissionFk = this.SubmissionFk, QuestionFk = this.QuestionFk, RevisionRequired = this.RevisionRequired }; // 深拷贝Answers列表 foreach (var answer in this.Answers) { cloned.Answers.Add(answer.Clone()); // 需要给SubmissionAnswerModel也实现Clone } return cloned; } }
然后创建原始集合时,逐个克隆对象:
m_originalSubmissionQuestions = new ObservableCollection<SubmissionQuestionModel>( _questionSection.Questions.Select(q => q.Clone()) );
方法2:使用序列化反序列化实现深拷贝
利用JSON或二进制序列化来快速实现深拷贝(需要确保所有Model都可序列化):
using System.Text.Json; private T DeepClone<T>(T obj) { var json = JsonSerializer.Serialize(obj); return JsonSerializer.Deserialize<T>(json); }
然后在加载数据时:
// 先深拷贝整个SubmissionModel或者单独拷贝Questions列表 var clonedQuestions = DeepClone<List<SubmissionQuestionModel>>(_questionSection.Questions); m_originalSubmissionQuestions = new ObservableCollection<SubmissionQuestionModel>(clonedQuestions);
方法3:手动复制属性(适合简单对象)
如果对象结构简单,也可以在创建副本时手动逐个复制属性:
m_originalSubmissionQuestions = new ObservableCollection<SubmissionQuestionModel>(); foreach (var question in _questionSection.Questions) { var clonedQuestion = new SubmissionQuestionModel { AnswerId = question.AnswerId, SubmissionFk = question.SubmissionFk, QuestionFk = question.QuestionFk, RevisionRequired = question.RevisionRequired }; // 复制Answers foreach (var answer in question.Answers) { clonedQuestion.Answers.Add(new SubmissionAnswerModel { // 复制SubmissionAnswerModel的属性 }); } m_originalSubmissionQuestions.Add(clonedQuestion); }
重置操作示例
当需要重置Model列表到原始状态时,同样需要用深拷贝的方式把原始集合的对象复制回去:
public void ResetToOriginal() { var questionSection = SubmissionModel.Sections.FirstOrDefault(); if (questionSection == null) return; questionSection.Questions.Clear(); foreach (var originalQuestion in m_originalSubmissionQuestions) { questionSection.Questions.Add(originalQuestion.Clone()); // 再次克隆,避免修改原始副本 } }
内容的提问来源于stack exchange,提问作者ob1Jakobi

