Laden...

Commandpattern mit Closures - Do/Undo/Redo implementieren

Erstellt von ErfinderDesRades vor 15 Jahren Letzter Beitrag vor 15 Jahren 4.596 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren
Commandpattern mit Closures - Do/Undo/Redo implementieren

Multilevel-Undo/-Redo mit dem Command-Muster fand ich ein spannendes Thema. Musste ich natürlich gleich auch aufziehen, habich mit Generika gemacht.

Nach Wiki: Command-Pattern ist ein Command nix als ein parametrisierter Aufruf, der getrennt von seiner Erstellung ausgeführt werden kann.
Nachteil des Command-Patterns sei, daß für jeden Command eine eigene Klasse zu schreiben sei.

Hier stelle ich eine Technik vor, die dieses Problem gelöst hat.

Basis meiner Commands sind Wiki: Closures, also Objekte, die einen Delegaten mit geeigneten Argumenten kapseln, und ihn ausführen können.
Demnach erfüllt so eine Closures eigentlich schon die Definition eines Commands.

Zur Implementierung von Do/Undo/Redo gehört natürlich nochn bischen mehr.
Meine Do/Undo/Redo-Commands bestehen im Kern aus einem Pärchen von Closures, eine für Redo, eine für Undo.
Die tatsächlich verwendeten Do/Undo/Redo-Commands halten dann gleich eine Liste solcher Pärchen, sodass ich mehrere Pärchen auch zu "Makro-Commands" zusammenfassen kann.
Beim Hinzufügen eines Redo/Undo-Pärchens zum Command führt dieses die Redo-Closure übrigens erstmalig aus.

Effekt: Ich brauch nicht für jede Aktion eine eigene Command-Klasse schreiben, sondern kann mir mit den generischen Closures jeweils das erforderliche Command maßgeschneidert zusammenstöpseln.

Die Do-/Undo-/Redo- Logik ist bei mir in einer Chain - Klasse implementiert. Das ist eine Art Liste, mit 2-fach verkettete Items, fast wie bei good old C (ohne +):


      private class Item {
         public Item Prev;
         public Item Next;
         public T Value;
      }

Chain wrappert so ein Item (nur eines!), und kann dem einen Nachfolger anhängen (Append).
Und wenns einen Nachfolger hat, kanns vorwärts laufen, und wenns einen Vorgänger hat, kanns auch zurück.
Das Anhängen ist übrigens recht gnadenlos: Falls angehängt wird, wo ein Nachfolger bereits besteht, wird dieser überschrieben (und die ganze Folge-Kette fällt dem GC anheim).
Chain.Append() geht ausserdem sofort einen Schritt vorwärts. Ergebnis: Nach einem Anhängen ist Undo (rückwärts laufen) enabled, und Redo (vorwärts laufen) versperrt, denn das neu geaddete Item hat ja keinen Nachfolger.

Damit hätten wir exakt die Do/Undo-/Redo- Logik: Das Erzeugen eines Commands führt Do erstmalig aus. Das Anhängen an die Chain läßt ggfs. vorher vorhandene Redos verfallen. Gleichzeitig wird Undo abrufbar, nämlich indem die Chain einen Schritt zurückgeht (wodurch wiederum Redo enabled wird, denn dann kann die Chain ja wieder vorwärts).

Validierung
Die Validierung findet nicht im Command selbst statt, sondern bevor ein Command gebildet wird, ist zu prüfen, obs auch ausführbar ist. Das ist im Grunde mit jedem Aufruf so. Wird ein Command in die Chain aufgenommen, so wird von seiner Validität ausgegangen. Aus der Validität der Redo-Closure ergibt sich logisch, daß die Undo-Closure auch valide ist, jedenfalls nach Ausführung der Redo-Closure.
Das ist eigentlich das schwierigste bei der Bildung eines Do/Undo/Redo-Commands: eine wasserdichte Undo-Closure zu bilden. Aber dieses Problem hat jede Implementation von Do/Undo/Redo.

Do/Undo/Redo funktioniert natürlich nur dann, wenn alle Aktionen über die Chain laufen. Wird ein per Command erstelltes ListboxItem anderwärts gelöscht, so erzeugt Command.Undo() natürlich einen Fehler.
Bei anderwärtigen Aktionen ist also jeweils die Chain zurückzusetzen.

Ich zeig mal den Form-Code der Sample-Solution, da sieht man ganz gut, wie (Do/Undo/Redo-)Commands zusammengestöpselt werden.
Implementiert sind Commands für Listbox.Add(Item), Listbox.Insert(Index, Item), Listbox.Remove(Item), und Listbox.ReplaceSelectedItem(Item).
Letzteres ist ein komplexes Command, zusammengesetzt aus RemoveAt(SelectedIndex) und Insert(SelectedIndex, Item)


using System;
using System.Windows.Forms;

namespace CommandPattern {

   public partial class frmCommandPattern : Form {

      private Chain<Command> _Commands = new Chain<Command>();

      public frmCommandPattern() {
         InitializeComponent();
         // kleines Extra: Chain<T> unterstützt DataBinding 
         btUnDo.DataBindings.Add(
            "Enabled", _Commands, "CanBackward", false, DataSourceUpdateMode.Never);
         btReDo.DataBindings.Add(
            "Enabled", _Commands, "CanForward", false, DataSourceUpdateMode.Never);
      }

      private void ItemInsert(ListBox lb, int indx, object itm) {
         lb.Items.Insert(indx, itm);
         lb.SelectedIndex = indx;
      }

      private void ItemRemoveAt(ListBox lb, int indx) {
         lb.Items.RemoveAt(indx);
         lb.SelectedIndex = indx < lb.Items.Count ? indx : indx - 1;
      }

      private Command CreateInsert(int indx, object itm) {
         // Closure.Create(ItemInsert, listBox1, indx, txtItem.Text) ist eine Umformung von:
         // ItemInsert(listBox1, indx, txt);
         var redo = Closure.Create(ItemInsert, listBox1, indx, itm);
         var undo = Closure.Create(ItemRemoveAt, listBox1, indx);
         return new Command(redo, undo); //der Command-Konstruktor führt redo erstmalig aus
      }

      private Command CreateRemoveAt(int indx) {
         var itm = listBox1.Items[indx];
         var redo= Closure.Create(ItemRemoveAt, listBox1, indx);
         var undo = Closure.Create(ItemInsert, listBox1, indx, itm);
         return new Command(redo, undo); 
      }
            
      // Schematische Erstellung eines Commands, unter Beachtung der Do-/Undo-/ReDo - Logik:
      // 1) validieren - kann Do ausgeführt werden?
      // 2) redo-Closure erzeugen - aus einem geeigneten Delegaten, mit geeigneten Argumenten
      // 3) undo-Closure erzeugen - dito
      // 4) der Command-Chain ein neues Command appenden, beinhaltend redo- und undo- Closure
      private void btInsert_Click(object sender, EventArgs e) {
         var indx = listBox1.SelectedIndex;
         var itm = txtItem.Text;
         if (indx < 0 || itm.Equals("")) {             // validieren
            MessageBox.Show("ItemInsert rejected!");
            return;
         }
         // Closure.Create(ItemInsert, listBox1, indx, txtItem.Text) ist eine Umformung von:
         // ItemInsert(listBox1, indx, txt);
         var redo = Closure.Create(ItemInsert, listBox1, indx, itm);
         var undo = Closure.Create(ItemRemoveAt, listBox1, indx);
         _Commands.Append(new Command(redo, undo)); // Command-Konstruktor führt redo erstmalig aus
      }

      private void btAdd_Click(object sender, EventArgs e) {
         var txt = txtItem.Text;
         if (txt == "") {
            MessageBox.Show("ItemAdd rejected!");
            return;
         }
         _Commands.Append(CreateInsert(listBox1.Items.Count,txt));
      }

      private void btRemove_Click(object sender, EventArgs e) {
         var indx = listBox1.Items.IndexOf(txtItem.Text);
         if (indx < 0) {
            MessageBox.Show("Remove rejected!");
            return;
         }
         _Commands.Append(CreateRemoveAt( indx));
      }

      // das Replace - Command kombiniert RemoveAt(indx) und Insert(indx, itm)
      private void btReplace_Click(object sender, EventArgs e) {
         var rejected = true;
         Command cmd = null;
         try {
            var indx = listBox1.SelectedIndex;
            if (indx < 0) return;                  // validieren
            cmd = CreateRemoveAt(indx);
            var itm = txtItem.Text;
            if (itm.Equals("")) return;               // validieren
            cmd.Combine(CreateInsert(indx, itm));
            _Commands.Append(cmd);
            rejected = false;
         }
         finally {
            if (rejected) {
               // wichtig! zeigt sich beim Kombinieren, dass das hinzuzufügenden Command nicht valide ist, so ist cmd.Undo() aufzurufen, um die halbfertigen Änderungen zu revidieren.
               if (cmd != null) cmd.Undo();
               MessageBox.Show("Replace rejected!");
            }
         }
      }

      private void btReDo_Click(object sender, EventArgs e) {
         var cmd = _Commands.Move(true);
         if (cmd == null) MessageBox.Show("Redo rejected!");
         else cmd.Redo();
      }

      private void btUnDo_Click(object sender, EventArgs e) {
         var cmd = _Commands.Move(false);
         if (cmd == null) MessageBox.Show("Undo rejected!");
         else cmd.Undo();
      }

   }
}

Schlüsselwörter: pattern,command,entwurfsmuster,command-pattern,gof,closure,undo

Der frühe Apfel fängt den Wurm.