C# 如何使用 MVVM 模式恢复对话框中的更改

C# How to revert changes in a dialog with the MVVM pattern

首先,我是一名 java 开发人员,目前正在帮助 C# 项目,因此在回答时请记住这一点。

我有以下场景。我有一个带有记录的数据网格,当我双击时,我打开了一个新的 window(dialog),其中包含一些字段以及一个保存和取消按钮。我想要取消按钮来恢复在对话框中所做的所有更改。

我想说这是一种非常常见的情况,但我必须在 Whosebug 上找到一个真正有效的答案,并且不需要大量的样板代码行。我不会在这里粘贴我的整个项目,我会尝试提供相关代码

Galaxy 和 SelectedJob 是简单的 JSON 可序列化的对象,仅具有 getter 和 setter。一个例子。 Job.cs

using System;
using System.Collections.Generic;

namespace GalaxyCreator.Model.Json
{
    [Serializable]
    public class Job
    {
        public String Id { get; set; }
        public String Name { get; set; }
        public Boolean StartActive { get; set; }
        public Boolean Disabled { get; set; }
        public Boolean Rebuild { get; set; }
        public Boolean Commandeerable { get; set; }
        public Boolean Subordinate { get; set; }
        public bool Buildatshipyard { get; set; } = true;
        public JobLocation JobLocation { get; set; }
        public JobCategory JobCategory { get; set; }
        public JobQuota JobQuota { get; set; }
        public IList<JobOrder> Orders { get; set; }
        public String Basket { get; set; }
        public String Encounters { get; set; }
        public String Time { get; set; }
        public Ship Ship { get; set; }
        public IList<String> Subordinates { get; set; }
    }
}

我的数据网格表单视图模型。

using System.Windows;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaxyCreator.Dialogs.DialogFacade;
using GalaxyCreator.Model.Json;

namespace GalaxyCreator.ViewModel
{
    class JobEditorViewModel : ViewModelBase
    {
        private IDialogFacade dialogFacade = null;
        private RelayCommand<object> _jobEditorDetailClickedCommand;

        public Galaxy Galaxy { get; set; }
        public Job SelectedJob { get; set; }

        public JobEditorViewModel(Galaxy Galaxy)
        {
            this.Galaxy = Galaxy;
            this.dialogFacade = new DialogFacade();
        }

        public RelayCommand<object> JobEditorClickedCommand
        {
            get
            {
                if (_jobEditorDetailClickedCommand == null)
                {
                    _jobEditorDetailClickedCommand = new RelayCommand<object>((param) => JobEditorClicked(param));
                }

                return _jobEditorDetailClickedCommand;
            }
        }

        private void JobEditorClicked(object param)
        {
            Dialogs.DialogService.DialogResult result = this.dialogFacade.ShowJobEditorDetail("Job Editor Detail", param as Window, this.SelectedJob);           
        }
    }
}

此视图模型的视图

<UserControl x:Class="GalaxyCreator.View.JobEditorView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:GalaxyCreator.View"
             xmlns:model="clr-namespace:GalaxyCreator.Model.JobEditor"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
             xmlns:cmd ="http://www.galasoft.ch/mvvmlight"            
             mc:Ignorable="d" 
             d:DesignHeight="1000" d:DesignWidth="1500">
    <Grid>
        <Border BorderBrush="Black" BorderThickness="1" Padding="5">
            <StackPanel Orientation="Vertical" HorizontalAlignment="Center" Width="Auto">
                <Label Content="Job Editor" FontSize="14" HorizontalContentAlignment="Center" Width="Auto"/>
                <DataGrid x:Name="JobDataGrid" 
                          ItemsSource="{Binding Path=Galaxy.Jobs}" 
                          SelectedItem="{Binding Path=SelectedJob, Mode=TwoWay}" 
                    AutoGenerateColumns="False" ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Auto" 
                    ScrollViewer.HorizontalScrollBarVisibility="Auto" Height="500">
                    <DataGrid.Columns>
                        <DataGridTextColumn x:Name="JobId" Binding="{Binding Id}" Header="Id" IsReadOnly="True" />
                        <DataGridTextColumn x:Name="JobName" Binding="{Binding Name}" Header="Name" IsReadOnly="True" />
                    </DataGrid.Columns>
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="MouseDoubleClick">
                            <cmd:EventToCommand Command="{Binding Path=JobEditorClickedCommand, Mode=OneWay}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </DataGrid>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

以下是一堆 类 将打开对话框并做一些样板文件以从对话框等中获取响应...它们并不重要我希望这个问题所以我把它们放在一边直接进入对话框窗体的视图模型。我一直在阅读大量关于纪念品模式的文章,并在某处保留了原始对象的深度克隆,并在取消时将其放回原处。这对我来说听起来像是可用的最干净的解决方案。似乎还有十几个类似 updatemode explicit 之类的......然后通常跟着 10000 行为什么 MVVM

不支持它
using GalaSoft.MvvmLight.Command;
using GalaxyCreator.Dialogs.DialogService;
using GalaxyCreator.Model.Json;
using System.Windows;

    namespace GalaxyCreator.Dialogs.JobEditor
    {
        class JobEditorDetailViewModel : DialogViewModelBase
        {
            private JobEditorDetailMemento memento;
            public Job Job { get; set; }

            private RelayCommand<object> _saveCommand = null;
            public RelayCommand<object> SaveCommand
            {
                get { return _saveCommand; }
                set { _saveCommand = value; }
            }

            private RelayCommand<object> _cancelCommand = null;
            public RelayCommand<object> CancelCommand
            {
                get { return _cancelCommand; }
                set { _cancelCommand = value; }
            }

            private JobOrder _selectedOrder;
            public JobOrder SelectedOrder
            {
                get { return _selectedOrder; }
                set
                {
                    if (_selectedOrder == value)
                        return;
                    _selectedOrder = value;
                    RaisePropertyChanged("SelectedOrder");
                }
            }

            public JobEditorDetailViewModel(string message, Job job) : base(message)
            {
                this.memento = new JobEditorDetailMemento(job);
                this.Job = job;
                this._saveCommand = new RelayCommand<object>((parent) => OnSaveClicked(parent));
                this._cancelCommand = new RelayCommand<object>((parent) => OnCancelClicked(parent));
            }

            private void OnSaveClicked(object parameter)
            {
                this.CloseDialogWithResult(parameter as Window, DialogResult.Yes);
            }

            private void OnCancelClicked(object parameter)
            {
                this.Job.Id = memento.Job.Id;
                this.CloseDialogWithResult(parameter as Window, DialogResult.No);
            }
        }
    }

这是此详细视图的缩写 xaml 代码

<UserControl x:Class="GalaxyCreator.Dialogs.JobEditor.JobEditorDetailView"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:GalaxyCreator.Dialogs.JobEditor"
      xmlns:model="clr-namespace:GalaxyCreator.Model.Json"
      xmlns:util="clr-namespace:GalaxyCreator.Util"    
      xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"             
      mc:Ignorable="d" d:DesignWidth="600">

    <Grid Margin="4">
        <Label Content="Id" />
        <TextBox Text="{Binding Path=Job.Id, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True}" />
        <Label Content="Name" Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Center" />
        <TextBox Text="{Binding Path=Job.Name, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True}" Grid.Column="3" x:Name="name" />

            <Button Name="btnSubmit" Content="Save" 
                Command="{Binding SaveCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"  Margin="0,0,0,5"/>

            <Button Name="btnCancel" Content="Cancel" 
                Command="{Binding CancelCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}" />
        </StackPanel>
    </Grid>
</UserControl>

看到的是这实际上在某种程度上起作用了。如您所见,我只使用 id 属性 进行测试。当我按下取消时,对话框关闭并且在内存中 属性 已更改。如果我双击数据网格上的同一行,对话框将打开并显示旧值。但是...数据网格上的 属性 已更改。所以在数据网格中你会看到我新输入的 id,而在内存中它包含旧的。我需要一种方法来通知我的数据网格嘿...请更新 属性 已更改。

所以最大的问题是:

对于你的第二个问题,我相信它基本上回答了你所问的一切,你使用实现 INotifyPropertyChanged 接口的 class 的 PropertyChanged 事件来表示应该呈现的变化。

看来您正在使用一些使用 RaisePropertyChanged 方法的程序包来执行我上面所说的操作。目前您只在一个 setter 上调用它,因此您需要在更多地方实施它。

好吧,正如@Ivan Ičin 在实施接口后回答的那样,它可以正常工作,但是转换为 MVVM Light 后,这就变成了

using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;

namespace GalaxyCreator.Model.Json
{
    public class Job : ObservableObject
    {
        private String _id;
        public String Id
        {
            get { return _id; }
            set
            {
                Set(ref _id, value);
            }
        }
    }
}

此外,ObservableObject 不可序列化!!!因此,如果您使用尝试过的序列化方法克隆您的对象,然后使用该字符串实例化一个新对象,它就无法工作。我使用了 json hack 来让它运行,可能不是最干净的解决方案但是因为我正在使用 json 无论如何......为什么不......

public static T CloneJson<T>(T source)
{
    if (Object.ReferenceEquals(source, null))
    {
        return default(T);
    }
    return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source));
}

我很高兴我让它工作了,thnx Ivan,但是我对我必须添加到我的简单样板的数量感到非常失望,因为我们会在 java POJO 对象中调用它。我理解提到的包 FoddyWeavers,它围绕 setter 编写样板代码,就像 Java 中的 AspectJ 一样。然而,这是一个额外的复杂层,我不想添加本质上是一个简单应用程序的东西。显示数据网格,在对话框中打开表单,使用取消编辑选项编辑内容。

C# 被说成是制作快速表单应用程序的更简单的解决方案,但作为 java 开发人员,到目前为止还没有让我信服或印象深刻。我本来可以用 JSF 或 JSP 更快地完成,那是古老的技术