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

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

一、引言

        这是关于用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 缺点

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

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现撤销操作的一种常用方式是使用命令模式。具体实现步骤如下: 1. 定义命令对象 首先,定义一个命令对象,该对象包含一个`execute`方法和一个`undo`方法,`execute`方法用于执行命令,`undo`方法用于撤销命令。例如: ```js class Command { constructor(execute, undo) { this.execute = execute; this.undo = undo; } } ``` 2. 定义命令历史记录 定义一个命令历史记录,用于存储所执行的命令。例如: ```js class CommandHistory { constructor() { this.commands = []; this.currentCommandIndex = -1; } execute(command) { command.execute(); this.commands.push(command); this.currentCommandIndex++; } undo() { if (this.currentCommandIndex >= 0) { const command = this.commands[this.currentCommandIndex]; command.undo(); this.currentCommandIndex--; } } redo() { if (this.currentCommandIndex < this.commands.length - 1) { this.currentCommandIndex++; const command = this.commands[this.currentCommandIndex]; command.execute(); } } } ``` 3. 创建命令对象并执行 创建一个命令对象,将其添加到命令历史记录中,并执行该命令。例如: ```js const command = new Command(() => { // execute command }, () => { // undo command }); const commandHistory = new CommandHistory(); commandHistory.execute(command); ``` 4. 撤销命令 使用命令历史记录的`undo`和`redo`方法来撤销命令。例如: ```js // 撤销上一步操作 commandHistory.undo(); // 上一步操作 commandHistory.redo(); ``` 以上是使用命令模式实现撤销操作的一种常见方式。需要注意的是,命令模式实现撤销操作只适用于命令式操作,而不适用于函数式操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值