撤销和重做实现-第二部分(命令模式)

Posted 火柴盒zhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了撤销和重做实现-第二部分(命令模式)相关的知识,希望对你有一定的参考价值。

        本文是从英文的网站翻译,用作自己的整理和记录,有修改调整。水平有限,欢迎指正。原文地址:地址

一、引言

        这是关于用C#编写多级撤销和重做实现的系列文章的第二部分。本系列展示了针对同一问题的三种方法的撤销/重做实现,以及我们如何使用这些方法实现不同场景的撤销/恢复。这些方法使用单对象状态变化、命令模式和备忘录模式。

       正如我们所知道的,撤销/重做没有通用的解决方案,撤销/重做的实现对于每个应用程序都具有定制化部分。因此,本系列文章的每个部分首先讨论了该模式下的设计思路,然后在实例程序中展示了如何实现。该系列,撤销/重做操作使用三种不同的设计方式,因此您可以将每个模式实现与其他方法实现进行比较,并选择最适合您需求的方法。在每个部分文章中我们还讨论了每种方法的优缺点。

二、Undo/Redo实现的基本思路

        我们知道,应用程序在每次操作后都会改变其状态。当应用程序操作运行时,它会改变其状态。因此,如果我们想Undo撤销,我们必须回到以前的状态。因此,为了能够恢复到以前的状态,我们需要存储应用程序运行时的状态。为了支持Redo重做,我们必须从当前状态转到下一个状态。

        要实现Undo/Redo,我们必须存储应用程序的状态,并且必须转到上一状态进行撤销,转至下一状态进行重做。因此,我们必须维护应用程序的状态以支持Undo/Redo。为了在三种设计方法中维护应用程序的状态,我们使用了两个堆栈。一个堆栈包含撤消操作的状态,第二个堆栈包含重做操作的状态。撤消操作弹出撤消堆栈以获取以前的状态,并将以前的状态设置为应用程序。同样,重做操作弹出重做堆栈以获得下一个状态,并为应用程序设置下一状态。

       现在我们知道,实现撤销-重做操作就是在应用程序的每个操作之后保持状态。现在的问题是,这种方法如何保持状态?在命令模式中,我们将单个操作的更改保留在ICommand对象中,该对象用于作为状态的特定操作类型。

        大家可以有兴趣的可以去看看设计模式中的命令模式

三、命令模式通用思路

        在以下步骤中讨论了如何使用命令模式设计思路:

3.1 第一步

        首先确定要支持 Undo/Redo 的操作。然后,确定在哪个容器中支持撤消/重做,以及要支持撤消/恢复的对象。

3.2 第二步

        为每个标识的操作创建从ICommand继承的command类。每个command 类将包含一个需要支持 Undo/Redo 属性。ICommand接口如下所示:

interface ICommand

       void Execute();
       void UnExecute();

        在Execute()方法中,您将使用命令的属性执行操作。在Unexecuted()方法中,您将使用命令的属性执行撤消操作。在这里,命令的属性包含相应命令进行撤消/重做操作所需的更改,以及更改对象的引用。

        特别注意:如果同一操作在不同对象上的行为不同,则必须为此操作发出多个命令。这是每个对象类型的操作命令。

3.3 第三步

      然后创建一个名为UndoRedo的类,该类包含两个堆栈。第一个用于撤消操作,第二个用于重做操作。此类实现了Undo方法、Redo方法和许多InsertInUnDoRedo方法,以在Undo/Redo系统中插入ICommand对象。调用InsertInUnDoRedo方法时,将ICommand对象插入到撤消堆栈中,以使操作撤消/重做启用并清除重做堆栈。

3.5.1 在每次撤消操作中:

  • 首先,您将检查撤消Undo堆栈是否为空。如果为空,则返回,否则继续。
  • 如果不是,则pop 从UndoStack 中弹出 ICommand
  • 然后把这个 command  push 到 RedoStack 
  • 然后,调用ICommand类中的Unexecute方法

3.5.2在每个重做操作中:

  • 首先,您将检查RedoStack堆栈是否为空。如果为空,则返回,否则继续。
  • 如果不是,则从RedoStack 弹出Icommand
  • Icommand push到Undostack
  • 然后,调用Icommand类中的execute方法

3.4 第四步

        当您在应用程序中执行不同的操作时,创建这个操作类型的Command类,并将其push撤消/重做系统。当您需要执行undo 撤消操作时,只需从应用程序中调用UndoRedo类的undo方法,当您需要进行重做操作时,则只需从您的应用程序调用redo 重做操作。

四、程序实现

        下面讨论了使用命令模式示例应用程序的撤消/重做实现:

5.1 第一步

        示例应用程序中有四个操作,分别是移动、调整大小、插入和删除。对象是矩形、椭圆,容器是WPF画布。

5.2 第二步

        现在我们将为继承ICommand接口的四个操作中的每一个创建四个命令类。

       5.2.1 MoveCommand 

class MoveCommand : ICommand
   
       private Thickness _ChangeOfMargin;
       private FrameworkElement _UiElement;

       public MoveCommand(Thickness margin, FrameworkElement uiElement)
       
           _ChangeOfMargin = margin;
           _UiElement = uiElement;
       

       #region ICommand Members

       public void Execute()
       
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left +
           _ChangeOfMargin.Left, _UiElement.Margin.Top
                   + _ChangeOfMargin.Top, _UiElement.Margin.Right +
           _ChangeOfMargin.Right, _UiElement.Margin.Bottom +
           _ChangeOfMargin.Bottom);
       

       public void UnExecute()
       
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left -
           _ChangeOfMargin.Left, _UiElement.Margin.Top -
               _ChangeOfMargin.Top, _UiElement.Margin.Right -
           _ChangeOfMargin.Right, _UiElement.Margin.Bottom -
           _ChangeOfMargin.Bottom);
       

       #endregion
   

        由于移动操作仅更改几何对象的边距,因此移动命令将包含边距的更改,即几何对象参考。在move命令中,Execute方法通过添加margin更改来完成对_UiElement几何对象的更改,而unexecutes方法通过减去已应用的更改来撤消操作。为此,它从几何对象UIelement中减去边缘变化。

5.2.2 ResizeCommand 

class ResizeCommand : ICommand
   
       private Thickness _ChangeOfMargin;
       private double _ChangeofWidth;
       private double _Changeofheight;
       private FrameworkElement _UiElement;

       public ResizeCommand(Thickness margin, double width,
           double height, FrameworkElement uiElement)
       
           _ChangeOfMargin = margin;
           _ChangeofWidth = width;
           _Changeofheight = height;
           _UiElement = uiElement;
       

       #region ICommand Members

       public void Execute()
       
           _UiElement.Height = _UiElement.Height + _Changeofheight;
           _UiElement.Width = _UiElement.Width + _ChangeofWidth;
           _UiElement.Margin = new Thickness
       (_UiElement.Margin.Left + _ChangeOfMargin.Left,
       _UiElement.Margin.Top
               + _ChangeOfMargin.Top, _UiElement.Margin.Right +
       _ChangeOfMargin.Right, _UiElement.Margin.Bottom +
       _ChangeOfMargin.Bottom);
       

       public void UnExecute()
       
           _UiElement.Height = _UiElement.Height - _Changeofheight;
           _UiElement.Width = _UiElement.Width - _ChangeofWidth;
           _UiElement.Margin = new Thickness(_UiElement.Margin.Left -
       _ChangeOfMargin.Left, _UiElement.Margin.Top -
           _ChangeOfMargin.Top, _UiElement.Margin.Right -
       _ChangeOfMargin.Right, _UiElement.Margin.Bottom -
       _ChangeOfMargin.Bottom);
       

       #endregion
   

        “调整大小”操作更改几何对象的边距、高度和宽度,因此“调整大小”命令可保存边距的更改、高度的更改、宽度的更改以及几何对象的引用。在resize命令中,Execute方法通过添加边距变化、高度变化和宽度变化来完成对_UiElement几何对象的更改。未执行方法通过减去已应用的更改来撤消操作。为此,它从几何对象_UIelement中减去边距变化、高度变化和宽度变化。

 5.2.3 InsertCommand 

class InsertCommand : ICommand
   
       private FrameworkElement _UiElement;
       private Canvas _Container;

       public InsertCommand(FrameworkElement uiElement, Canvas container)
       
           _UiElement = uiElement;
           _Container = container;
       

       #region ICommand Members

       public void Execute()
       
           if (!_Container.Children.Contains(_UiElement))
           
               _Container.Children.Add(_UiElement);
           
       

       public void UnExecute()
       
           _Container.Children.Remove(_UiElement);
       

       #endregion
   

        插入操作是在面板中插入几何对象,插入命令保存几何对象和画布的引用。在insert命令中,Execute方法将几何对象添加到画布,Unexecute方法从画布中删除几何对象。

5.2.4 DeleteCommand 

class DeleteCommand : ICommand
    
        private FrameworkElement _UiElement;
        private Canvas _Container;

        public DeleteCommand(FrameworkElement uiElement, Canvas container)
        
            _UiElement = uiElement;
            _Container = container;
        

        #region ICommand Members

        public void Execute()
        
            _Container.Children.Remove(_UiElement);
        

        public void UnExecute()
        
            _Container.Children.Add(_UiElement);
        

        #endregion
     

        当删除操作从面板中删除几何对象时,delete命令保存几何对象和画布的引用。在delete命令中,Execute方法从画布中删除几何对象,而Unexecute方法将几何对象添加到画布中。

5.3 第三步

        现在我们将根据通用模型的描述实现UndoRedo类。 

public class UnDoRedo
  
      private Stack<ICommand> _Undocommands = new Stack<ICommand>();
      private Stack<ICommand> _Redocommands = new Stack<ICommand>();

      private Canvas _Container;

      public Canvas Container
      
          get  return _Container; 
          set  _Container = value; 
      

      public void Redo(int levels)
      
          for (int i = 1; i <= levels; i++)
          
              if (_Redocommands.Count != 0)
              
                  ICommand command = _Redocommands.Pop();
                  command.Execute();
                  _Undocommands.Push(command);
              

          
      

      public void Undo(int levels)
      
          for (int i = 1; i <= levels; i++)
          
              if (_Undocommands.Count != 0)
              
                  ICommand command = _Undocommands.Pop();
                  command.UnExecute();
                  _Redocommands.Push(command);
              

          
      

      #region UndoHelperFunctions

      public void InsertInUnDoRedoForInsert(FrameworkElement ApbOrDevice)
      
          ICommand cmd = new InsertCommand(ApbOrDevice, Container);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      

      public void InsertInUnDoRedoForDelete(FrameworkElement ApbOrDevice)
      
          ICommand cmd = new DeleteCommand(ApbOrDevice, Container);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      

      public void InsertInUnDoRedoForMove
      (Point margin, FrameworkElement UIelement)
      
          ICommand cmd = new MoveCommand(new Thickness
          (margin.X, margin.Y, 0, 0), UIelement);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      

      public void InsertInUnDoRedoForResize
  (Point margin, double width, double height, FrameworkElement UIelement)
      
          ICommand cmd = new ResizeCommand(new Thickness
      (margin.X, margin.Y, 0, 0), width, height, UIelement);
          _Undocommands.Push(cmd);_Redocommands.Clear();
      

      #endregion
  

        第一堆栈_Undocommands持有可撤销操作,第二堆栈 _Redo commands持有redoable 操作。当InsertInUnDoRedo 方法被调用时,Icommand对象被插入到_Undocommands栈中,以使命令成为可撤销操作,并清除重做堆栈。此处级别决定您要执行撤消的次数。

5.4 第四步 

         在你的应用程序中执行不同的操作时,给这个操作定义一个Command object,使用InsertInUnDoRedo方法新增到Undo/Redo系统。当从UI单击Undo时,我们调用UndoRedo类的Undo方法,当从UI中单击Redo时,我们将调用UndoRedo类的Redo方法。

        在这里,没有明确设置撤消堆栈和重做堆栈的大小,保存的撤消重做状态的数量取决于系统内存。

六、优势和缺点

6.1 优点

        其可维护性良好,不包含任何冗余信息。它不是内存密集型(对比第三种方法)

6.2 缺点

        命令模式的唯一缺点是,无论操作大小,都必须使命令类型等于操作数。随着操作的增加,命令也会增加

以上是关于撤销和重做实现-第二部分(命令模式)的主要内容,如果未能解决你的问题,请参考以下文章

撤销和重做实现-第一部分(单对象状态变化)

撤销和重做实现-第一部分(单对象状态变化)

撤销和重做实现-第三部分(备忘录模式)

撤销和重做实现-第三部分(备忘录模式)

命令模式-command

B6:命令模式 Command