You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 11:45:56