C#使用命令模式实现撤销和恢复功能

第一次写关于设计模式的随笔,最近在使用C#做一个WinForm的项目,其中要求需要支持撤销和恢复功能,想到了以前看过Command模式支持撤销和恢复操作,就在项目中使用了。对命令模式理解的不够深,各位看客请指正。

Gof23种设计模式中的Command模式,其意图是这么描述的“将一个请求封装为一个对象,从而是你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作”;另外个人的理解就是可以将调用者和接受者解耦出来。下图为Command的类图:

首先理解将调用者和接受者解耦出来:Invoker中只需要持有一个Command接口的引用即可,但是具体是其哪一个子类,Invoker并不知道。而真正执行动作的接受者,Invoker并不用知道,这样就讲调用者和接收者解耦出来。另外理解实现撤销和恢复操作。程序需要将所有操作过的命令对象存储起来,存储的每一个命令对象保存了其当前状态和上一个状态,因此可用来做撤销和恢复操作。下面用代码进行说明,本文使用C#语言实现的。

    首先,我们定义一个Command的接口。定义如下:

interface Command
{
     void execute();
     void undo();
 }

其中仅仅声明了两个函数,其如果在c++的实现的话,使用纯虚类。

我们要实现的具体功能为,可以为一个TextBox控件实先撤销和恢复功能。因此,我们实先一个TextChangedCommand的类,定义如下:

 1 public class TextChangedCommand: Command
 2 {
 3      private TextBox ctrl;
 4      private String newStr;
 5      private String oldStr;
 6 
 7      public TextChangedCommand(TextBox ctrl, String newStr, String oldStr)
 8      {
 9          this.ctrl = ctrl;
10          this.newStr = newStr;
11          this.oldStr = oldStr;
12      }
13 
14      public void execute()
15      {
16          this.ctrl.Text = newStr;
17          this.ctrl.SelectionStart = this.ctrl.Text.Length;
18      }
19 
20      public void undo()
21      {
22          this.ctrl.Text = oldStr;
23          this.ctrl.SelectionStart = this.ctrl.Text.Length;
24      }
25 }

其中有三个成员变量,分别为TextBox类型的ctrl,和String类型的newStr、oldStr。这里的ctrl就可以理解为类图中的Invoker,而newStr和oldStr即为当前状态和上一个状态。

由于我们需要实现其撤销和恢复功能,因此这里创建两个栈对象,分别用来存储已经执行了的命令对象和撤销后的命令对象。即在撤销动作中,从栈中弹出一个Command对象,然后执行对象的undo操作,执行完成后将其存储到恢复的栈中,在点击恢复时,用从恢复栈弹出一个Command对象,执行其execute动作。两个栈的定义如下:

Stack<Command> undoStack = new Stack<Command>();
Stack<Command> redoStack = new Stack<Command>();

下面实现类图中的Client部分,其主要功能是创建一个具体的Command对象,同时为为其设置接收者。对于一个TextBox来说,在其Text熟悉变化的时候,将其上一个状态和当前状态保存下来。具体试下如下:

private void textBox1_TextChanged(object sender, EventArgs e)
         {
if(flag){ TextChangedCommand com
= new TextChangedCommand((TextBox)textBox1, ((TextBox)textBox1).Text, oldStr); undoStack.Push(com); oldStr = ((TextBox)textBox1).Text;
} }

这个函数是控件textBox1的Text属性变化时的回调函数。在其中创建了Command基类的实力,并将其存到栈对象undoStack中。下面来做恢复和撤销操作的代码:

撤销操作:

 private void button1_Click(object sender, EventArgs e) //撤销
{
      if (undoStack.Count == 0)
          return;
    
flag = false;
Command com
= undoStack.Pop(); com.undo(); redoStack.Push(com); }

恢复操作:

private void button2_Click(object sender, EventArgs e) //恢复
         {
             if (redoStack.Count == 0)
                 return;

flag = false;
             Command com = redoStack.Pop();
             com.execute();

             undoStack.Push(com);
         }

我将两个操作分别放到两个button的操作里面,一个用于撤销,另外一个用于恢复。由于我们在进行撤销和恢复时,在TextChangedCommand中是对TextBox控件进行赋值,因此也会触发textBox1控件的Text属性变化的回调函数,我这这里使用了一个标志flag,如果是在撤销和恢复的动作中,则不做Command对象的压栈动作。到这里,已经完全可以实现了TextBox的撤销和恢复功能,当然在我自己的项目中,出了TextBox控件为,还有ComboBox、DateTimePicker、CheckBox控件都实现了撤销和恢复功能,但是ComboBox控件的回调函数需要使用SelectIndexChanged作为回调,不要使用SelectTextChanged,因为当Index变化时也会触发Text属性的变化。

这里简单介绍一下在撤销恢复时,不触发textBox1的Text的属性回调函数的另外一种方法。我在这里将其称之为原子操作。可以将textBox1的回到函数textBox1_TextChanged作为一个参数,传入到Command对象中。在对TextBox的Text属性变化前先做这个动作:ctrl.TextChanged -=动作,赋值完成后,再做Ctrl.TextChanged +=动作。下面用一个函数简单模拟一下:

void setTextBox(TextBox ctrl, String str,  EventHandler eventhandler)

{

  ctrl.TextChanged -= eventhandler;

     ctrl.Text = str;

     ctrl.TextChanged += eventhandler;

}

 附上源码:https://files.cnblogs.com/files/youyipin/test_Charp.zip

posted @ 2016-09-30 14:03  太极者  阅读(13563)  评论(14编辑  收藏  举报