Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Undo/Redo mit MVVM
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

Undo/Redo mit MVVM

beantworten | zitieren | melden

Hallo allerseits,

seit einiger Zeit mache ich mir Gedanken über die Implementierung eines Undo/Redo-Patterns. Das erste Problem habe ich bereits gelöst, nämlich die Zustände eines Objektes laden und speichern zu können. Das passiert einfach über XML-Daten, d.h. man braucht für jeden Verarbeitungsschritt nur das veränderte Objekt zu speichern sowie die XML Daten, um den ursprünglichen Zustand wiederherstellen zu können.

Gepeichert werden die Zustände dann in zwei Stacks im MainViewModel, einen für die verfügbaren Undo-Vorgänge, und einen für die Redo-Vorgänge.

Bis hier ist es noch relativ trivial und man kann das alles irgendwo nachlesen. Aber die weitere Vorgehensweise bleibt für mich halbwegs im Dunkeln. Vom Undo/Redo-System müssen folgende Operationen "mitgeloggt" werden:
- Property-Änderungen
- Änderung von Listen (ObservableCollections)
- Commands, die mehrere der o.g. Änderungen auf einmal vornehmen

Das Hauptproblem besteht darin zu unterscheiden, ob eine Änderung innerhalb eines Commands passiert, oder durch eine einfache Änderung eines Wertes (z.B. in einer Textbox mit gebundenen Property). Die wichtigste Frage ist also, wo die Änderungen am besten "geloggt" werden, um solche Mehrdeutigkeiten auszuschließen.

Und apropos Mehrdeutigkeiten: Es ist ja auch so, daß Textboxen ein eigenes Undo/Redo-System haben, so daß der Benutzer zwei Möglichkeiten hat, seine Eingabe rückgängig zu machen. Wenn er Strg-Z verwendet (oder das ContextMenu der Textbox), dann bleiben die Undo-Zustände des MainViewModels erhalten. Das würde aber früher oder später den Benutzer verwirren. Gibt es dafür eine geeignete Vorgehensweise?

Danke schonmal,
Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
talla
myCSharp.de - Experte

Avatar #avatar-3214.jpg


Dabei seit:
Beiträge: 7290
Herkunft: Esslingen

beantworten | zitieren | melden

Hallo,

ich verstehe nicht ganz wieso du unterscheiden willst, welche Aktion die Änderung durchgeführt hat. Es geht ja nur um Zustände deines Objektes. Wenn du den vorherigen Zustand wieder herstellst, dann ist doch alles wie vorher, egal wie es zu dem Zustandswechsel zuvor gekommen ist.

Zu der TextBox: Wenn du das Binding so gestaltest, dass es nicht bei LostFocus, wie es bei TextBoxen Standard ist, sondern bei PropertyChanged aktualisiert wird, dann ist die TextBox ja immer synchron zum ViewModel. Ein Undo/Redo in der TextBox würde demnach ja auch nur einen normalen Zustandswechsel des VM entsprechen, den der Nutzer ganz normal bei Undo rückgängig machen könnte.

Im allgemeinen gibts dafür auch noch das IEditableObject Interface, das DataGrid unterstützt das z.B.
Baka wa shinanakya naoranai.

Mein XING Profil.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hi talla,
Zitat von talla
ich verstehe nicht ganz wieso du unterscheiden willst, welche Aktion die Änderung durchgeführt hat. Es geht ja nur um Zustände deines Objektes. Wenn du den vorherigen Zustand wieder herstellst, dann ist doch alles wie vorher, egal wie es zu dem Zustandswechsel zuvor gekommen ist.

Das ist ja gerade das schwierige, daß es für den Benutzer nicht egal ist. Wenn er ein Property in einer Textbox geändert hat, sollte die Undo-Aktion dann auch "Parameter Change" oder so heißen. Wenn das gleiche Property durch ein Command geändert wurde, soll die Aktion auch so heißen wie das Kommando (z.B. "Apply Template" o.ä.). Wichtig ist halt, daß dann nicht X mal in der Undo-List "Parameter Change" vorkommt, das würde den Benutzer verwirren und wäre nicht besonders hilfreich.

Das heißt im Grunde, daß bei einigen Aktionen mehrere (Unter)-Aktionen für den Benutzer zusammengefaßt werden müssen. Deshalb kann man nicht einfach ganz naiv für jede Property-Änderung eine Undo-Aktion erstellen...

Viel komplizierter wird es bei Operationen mit Auflistungen.
Zitat von talla
Zu der TextBox: Wenn du das Binding so gestaltest, dass es nicht bei LostFocus, wie es bei TextBoxen Standard ist, sondern bei PropertyChanged aktualisiert wird, dann ist die TextBox ja immer synchron zum ViewModel. Ein Undo/Redo in der TextBox würde demnach ja auch nur einen normalen Zustandswechsel des VM entsprechen, den der Nutzer ganz normal bei Undo rückgängig machen könnte.

Aber die Undo-Redo-Ereignisse des ViewModels und der TextBox sind nicht aneinander gebunden. D.h. wenn ich eins davon rückgängig mache, wird nicht automatisch das andere rückgängig gemacht. Der Wert wird zwar zurückgesetzt, aber die Undo/Redo-Lsiten sind dann nicht mehr synchronisiert.
Zitat von talla
Im allgemeinen gibts dafür auch noch das IEditableObject Interface, das DataGrid unterstützt das z.B.

Dieses Pattern ist ein guter Ansatz. Wenn jede Benutzeraktion ein Objekt explizit in einen Bearbeitungszustand (und zurück) versetzen muß, dann wäre das Undo/Redo relativ einfach zu implementieren. Dann muß man aber dafür sorgen, daß die Eingabe einer Textbox (oder anderer Steuerelemente) die Objekte in den Bearbeitungszustand versetzt. Dafür fällt mir allerdings momentan keine "schöne" Lösung ein...

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
Blacal
myCSharp.de - Member



Dabei seit:
Beiträge: 392

beantworten | zitieren | melden

Hallo MrSparkle,

vor der Überlegung stand ich vor kurzem auch.
Habe bei einer Windows.Forms Anwendung, die relativ viel auf DataBindings setzt, das "klassische" Command-Pattern umgesetzt und musste feststellen, dass das je nach Situation schon relativ kompliziert werden kann, wenn man es falsch macht. Relativ viel habe ich mit "ChangeTracker"-Componenten gemacht. Dabei handelt es sich um Komponenten, die selbst auf PropertyChanged-Ereignisse horchen und für Änderungen automatisch Command-Objekte erzeugen. Für komplexere Befehle gab es bei dieser Lösung einfach eigene Command-Klassen.

Am Ende war es aber wie gesagt an manchen Stellen zwar richtig cool, an wenigen Stellen auch relativ kompliziert. Zum Beispiel der Fall, dass die Änderung eines Properties automatisch eine Neuberechnung an einer anderen Stelle triggert. Das so richtig zu kombinieren, dass bei einem Undo alles wieder zurückgesetzt wird, war vom Konzept her nicht leicht.

Was für dich eventuell noch interessant ist, ist dieses Projekt:
Monitored Undo Framework

Die verfolgen einen Ansatz, bei dem die Undo/Redo-Befehle dynamisch je nach getaner Änderung erzeugt werden. Auf der anderen Seite müsste es auch möglich sein, alle Änderung innerhalb einer bestimmten Methode zu "tracken" und dann als eigenen Undo/Redo-Befehl abzuspeichern. Geht denke ich in die Richtung, die du suchst, habe es mir aber noch nicht genauer angeschaut.

An sich ein leicht zu unterschätzendes Thema. Bin gespannt, welche Lösung zu nutzen wirst.

Gruß
Roland
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hallo allerseits,

danke für die vielen Anregungen. Ich habe mir viele verschiedene Lösungen angeschaut, bin nochmal in mich gegangen und halte die "Change-Tracking"-Variante eigentlich für die beste Lösung.

Die ViewModel-Klassen haben im allgemeinen nämlich schon alle Methoden implementiert, die man benötigt, um die Änderungen zu verfolgen:
- OnPropertyChanged (dank der INotifyPropertyChanged-Schnittstelle)
- OnCollectionChanged (INotifyCollectionChanged)

Beide Methoden rufen in meiner Implementierung eine OnInvalidated-Methode auf, so daß man nur eine einzige Stelle hat, wo man auf Änderungen reagieren muß. Ein kleines Problem ist allerdings, daß OnPropertyChanged erst aufgerufen wird, nachdem der Wert des Properties geändert wurde. D.h. man muß bei der Initialisierung des ViewModel-Objektes einen "Ursprungszustand" an das Undo/Redo-System übergeben.

Das andere Problem ist, wie man nun alle getrackten Änderungen zu einer Undo-Aktion zusammenfaßt. Das wird in dem von Blacal genannten Monitored Undo Framework als UndoBatch bezeichnet, und sieht so aus:


// Zwei Änderungen ergeben einen Undo-Schritt:
using (new UndoBatch(Document1, "Change Name", false))
{
    Document1.A.Name = firstChange;
    Document1.A.Name = secondChange;
} 

Eine ziemlich elegante Lösung, finde ich. Das ganze kann man nämlich schachteln, und es wird jeweils nur die "äußere" Undo-Aktion gezeigt. Wenn also viele "Property Changed"-Aktionen innerhalb eines UndoBatch auf Command-Ebene stattfinden, wird nur die Rückgängig-Aktion für das Command angezeigt.

Jetzt braucht man nur noch eine eigene Command-Klasse, die das auch gleich mit implementiert. Dann hat man ein prima Undo-Redo-Sytem für seine ViewModel-Klassen. Die Basisklasse überwacht die Änderungen per OnInvalidated-Methode, die Command-Klasse faßt alle Änderungen pro Kommando in einen Undo-Schritt zusammen. Und das MainViewModel verwendet irgendeine Stack-basierte Verwaltung für das Ganze.

Ich glaube, da haben wir eine einfache, aber umso elegantere Methode gefunden, ein Undo/Redo-Sytem zu implementieren, das vor allen Dingen auch wiederverwendbar ist. Denn der große Vorteil liegt meiner Meinung nach darin, daß man bei der Implementierung eigener ViewModels nur noch von der Basisklasse ableiten, und sich um die Undo/Redo-Geschichte keine Gedanken machen muß.

Bin gespannt, ob ich nicht irgendetwas übersehen habe. Ich werd mich in der nächsten Zeit mal an die Implementierung wagen und berichten.

Schöne Grüße,
Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hallo nochmal,

das Konzept funktioniert leider noch nicht so ganz. Die Ursache ist wohl, daß ich die Objekte als XML bzw. Binärdaten serialisieren möchte, um einen bestimmten Objektzustand zu speichern.

Probleme würde es aber dann geben, wenn in einem Schritt ein Objekt geändert und im nächsten Schritt dann gelöscht wird. Der erste Undo-Schritt müßte dann das Löschen des Objektes rückgängig machen. In diesem Fall wurde das betreffende Objekt ja aus einer Auflistung in einem anderen, übergeordneten Objekt entfernt. Daher würde sich der Undo-Mechanismus den Zustand des übergeordneten Objektes merken. Beim Undo wird dann dessen Zustand wiederhergestellt, daher ist dann das gelöschte Objekt wieder in der Liste an der richtigen Stelle zu finden.

Beim zweiten Undo-Schritt müßte dann die Änderung des Objekts rückgängig gemacht werden, und dann weiß der Undo-Mechanismus nicht mehr, auf welches Objekt sich das bezieht. Denn das Objekt ist durch die Serialisierung nicht mehr das gleiche wie vor dem Löschen.

Normalerweise würde man sich die Referenz des gelöschten Objektes merken, und das beim Rückgängigmachen dann wieder an der richtigen Stelle in die richtige Auflistung einfügen. Aber soetwas umzusetzen ist wesentlich komplizierter und fehleranfälliger als die Variante, wo man die Objektzustände einfach als XML speichern kann. Gibt es dafür eine andere Lösung?

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
winSharp93
myCSharp.de - Experte

Avatar #avatar-2918.png


Dabei seit:
Beiträge: 6155
Herkunft: Stuttgart

beantworten | zitieren | melden

Hallo MrSparkle,

ich persönlich halte bei Undo/Redo das Memento-Pattern für die sinnvollste Umsetzung.

Bei jedem "Commit" einer Objektänderung (in deinem Fall beim Disposen des UndoBatches) wird der Status des Objektes gesichert.
Dieser Status kann dann serialisiert werden und kann direkt wiederhergestellt werden.

Für eine Collection enthält das Memento-Objekt dann eine Collection der Memento-Objekte der Children sowie eine Kopie eines Arrays mit den ursprünglichen Elementen (wird nicht serialisiert, sondern bei Deserialisierung neu angelegt!).
Soll ein State dann wiederhergestellt werden, wird einfach die komplette Collection quasi ersetzt und danach die States wiederhergestellt.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hi winSharp93,

in diesem Fall müßten dann aber die Referenzen der Objekte in der Auflistung gespeichert werden, oder? Da kommt man mit Serialisierung wahrscheinlich doch nicht weiter.

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
winSharp93
myCSharp.de - Experte

Avatar #avatar-2918.png


Dabei seit:
Beiträge: 6155
Herkunft: Stuttgart

beantworten | zitieren | melden

Zitat von MrSparkle
Da kommt man mit Serialisierung wahrscheinlich doch nicht weiter.
Doch: Du musst diese nur nicht mitserialisieren; beim Deserialisieren kannst du die Objekte dann ja anhand der serialisierten Mementos neu erzeugen.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hi winSharp93,

aber genau das ist ja mein Problem!

Denn wenn nach dem Undo ein anderes Objekt vorhanden ist, als vorher, können auch frühere Änderungen daran nicht mehr rückgängig gemacht werden (s.o.)

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
winSharp93
myCSharp.de - Experte

Avatar #avatar-2918.png


Dabei seit:
Beiträge: 6155
Herkunft: Stuttgart

beantworten | zitieren | melden

Ich glaube, wir reden gerade ein wenig aneinander vorbei
Zitat von MrSparkle
Denn wenn nach dem Undo ein anderes Objekt vorhanden ist, als vorher, können auch frühere Änderungen daran nicht mehr rückgängig gemacht werden
Wie gesagt: Das Memento eines Elementes mit Children enthält sowohl eine Referenz auf die ursprünglichen Children als auch deren Mementos.
Bei Deserialisierung werden die Children einfach per new erstellt und das Memento zurückgespielt.

Eigentlich ist es ja eh' egal, ob du das gleiche Objekt wiederherstellst oder nicht - durch das Memento wird es ja wieder in den gewünschten Zustand versetzt.
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hallo allerseits,

ich würde den Beitrag gerne nochmal ausgraben. Meine Implementierung mit der Serialisation war leider eine Sackgasse. Ich bin daher auf der Suche nach einer eleganteren und funktionierenderen Lösung.

Welchen Ansatz verwendet ihr in euren WPF-Anwendungen, um eine Undo-Redo-Funktion möglichst einfach im ViewModel zu implementieren?

Schöne Grüße,
Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
Blacal
myCSharp.de - Member



Dabei seit:
Beiträge: 392

beantworten | zitieren | melden

Hi,

was hast du da genau für ein Problem?
Was mich auch interessieren würde, wäre, inwieweit du eigentlich Serialisierung verwendest. In meinen Lösungen dafür wäre ich noch nie auf den Gedanken gekommen, überhaupt etwas während des Undo/Redo zu serialisieren oder zu deserialisieren. Bei mir läuft Undo/Redo eigentlich immer über die selben Objekte.

Mit meiner Lösung - wie weiter oben beschrieben - bin ich mitlerweile eigentlich recht zufrieden.

Gruß
Roland
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hi,

das mit der Serialisierung hat sich als sehr unpassend herausgestellt, deswegen suche ich eine Alternative. Serialisierung fand ich anfangs gut, weil man dann jederzeit den kompletten Zustand eines Objektes speichern und wiederherstellen kann. Aber es werden bei jedem Redo-Vorgang neue ViewModel-Objekte und dadurch neue View-Objekte erzeugt. Selektionen bleiben dadurch beispielsweise nicht erhalten und der Benutzer wird verwirrt. Deswegen sollte man wohl unbedingt mit den ViewModel-Objekten arbeiten.

Es ist für mich jedenfalls noch nicht klar, wie ich die Veränderungen tracke, wenn beispielsweise ein Command mehrere Propertys und Listen ändert. Ich hab ja bestimmte Properties an Steuerelemente gebunden, mit denen sie direkt geändert werden können. Dann hab ich ItemControls, die Listen verändern können. Und ich hab Commands (aufgerufen über Menüs oder Buttons), die beides machen. Für das Undo-System ist es aber etwas anderes, ob ein Property durch den Benutzer oder durch ein Command verändert wurde.

Es gibt ja neben dem genannten Monitored Undo Framework auch andere Frameworks, die aber alle komplett unterschiedliche Herangehensweisen haben. Jedes hat seine Stärken und Schwächen, aber keins deckt meine Anwendungsfälle so ab, daß ich mir wirklich Arbeit dadurch sparen würde.

Hat evtl. schonmal jemand Erfahrungen mit dem einen oder anderen UndoRedo-Framework gemacht? Welche Herangehensweise habt ihr verfolgt, um in möglichst wenigen Klassen möglichst wenig Code hinzufügen und warten zu müssen?

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
Ahrimaan
myCSharp.de - Member



Dabei seit:
Beiträge: 363
Herkunft: Thorn

beantworten | zitieren | melden

Evtl. stelle ich mir das zu einfach vor aber :

Ein Interface/Basisklasse welche jeder seiner Propertyes privates etc. per Reflection Monitored:

Beispiel:

Es wird ein Command abgeschickt . Dieses löst wiederrum ein Event aus der Basisklasse aus "merkezustand"
Dann läuft eine Methode alle Properties durch und "merkt" sich den Zustand im Speicher und einem Statecounter und evtl. weitere Infos.

Hmm da müsste man sich mal Gedanken machen.... Wir hatten bei uns einen ähnlichen Ansatz verfolgt, ich schaue mal morgen Abend in den alten Code.

P.S. Undo/Redo Applikationsweit oder "Fensterweit" ?
Dieser Beitrag wurde 2 mal editiert, zum letzten Mal von Ahrimaan am .
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Zitat von Ahrimaan
Dann läuft eine Methode alle Properties durch und "merkt" sich den Zustand im Speicher und einem Statecounter und evtl. weitere Infos.

Da gibt es zwei Möglichkeiten, entweder merkt man sich Zustände oder man merkt sich Veränderungen. Besonders beim Arbeiten mit Listen hab ich festgestellt, daß es wesentlich eleganter ist, sich die Veränderungen (hier also Einfügen oder Löschen von Elementen) zu merken. Viel wichtiger finde ich aber, wie die einzelnen Änderungen zu einem Undo-Kommando zusammengefaßt werden können.

Was verstehst du übrigens unter "per Reflection Monitored"? Eine ständige Überwachung der aktuellen Zustände der ViewModels?
Zitat von Ahrimaan
Hmm da müsste man sich mal Gedanken machen.... Wir hatten bei uns einen ähnlichen Ansatz verfolgt, ich schaue mal morgen Abend in den alten Code.

Das würde mich sehr interessieren...
Zitat von Ahrimaan
P.S. Undo/Redo Applikationsweit oder "Fensterweit" ?

Anwendungsweit, bzw. "MainViewModel"-weit :)

Christian
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
Ahrimaan
myCSharp.de - Member



Dabei seit:
Beiträge: 363
Herkunft: Thorn

beantworten | zitieren | melden

Zitat von MrSparkle
Was verstehst du übrigens unter "per Reflection Monitored"? Eine ständige Überwachung der aktuellen Zustände der ViewModels?

Christian

Eine ständige Überwachung wäre im MVVM evtl. übertrieben.
Das PropertyChanged reicht aus, um sich Werte zu merken.
Reflection würde ich nur nutzen, damit ich nicht jede Property händisch angeben muss.
Wobei ein Ansatz wo ich im Konstruktor angebe, welche Properties/Variablen überwacht werden sollen sicher auch nicht schlecht wäre ;-)
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von Ahrimaan am .
private Nachricht | Beiträge des Benutzers
Blacal
myCSharp.de - Member



Dabei seit:
Beiträge: 392

beantworten | zitieren | melden

Hi,

ich habe jetzt die letzten paar Beiträge nur überflogen, aber noch ein Gedanke von mir:
Bei meinen Programmen hatte ich das Gleiche Problem. Ich hatte mehrere DataGrids und diverse Masken, die jeweils ein einzelnes Objekt bearbeiten. Für das Change-Tracking habe ich dabei nicht die ViewModel- oder Model-Objekte überwacht, sondern die View. Ich habe also Observer-Objekte, welche sich an die Ereignisse von TextBoxen, ComboBoxen, DataGrids usw. hängen können und dafür sorgen, dass für jede Änderung ein entsprechender Command erzeugt wird, welche die Änderung wieder Rückgängig machen kann.

Nachteil an dem Vorgehen ist, dass man in jedem Oberflächen-Control auch das Change-Tracking mit einbauen muss. Vorteil ist, dass man direkt auf die einzelnen Controls und dabei auf die Aktionen des Benutzers eingehen kann. Trackt man das ViewModel oder das Model selbst, so stelle ich mir es schwer vor, die einzelnen Änderungen auch zu Aktionen des Users zuzuordnen, welche ja oft mehrere Änderungen am Model bewirken können.

Achja: Dieses Vorgehen gilt hauptsächlich für alle Eingabeflächen im Programm. Für Buttons o. Ä., welche irgendwas machen, erstelle ich je nach Anforderung ein entsprechendes Command - auch wenn dieses nur aus mehreren Unter-Commands besteht.

Gruß
Roland
private Nachricht | Beiträge des Benutzers
Ahrimaan
myCSharp.de - Member



Dabei seit:
Beiträge: 363
Herkunft: Thorn

beantworten | zitieren | melden

Hey zusammen,

also die Musterlösung von damals finde ich nicht mehr, der alte Source liegt bei meiner alten Firma und die mag mich seit meinem Weggang nicht mehr ;-)
Mein Kollege und ich haben gestern Ideen gesammelt und werden uns an die Umsetzung machen und dann hier als Snippet veröffentlichen.

Wir werden folgenden Ansatz verfolgen :
Eine Klasse (entweder als Basisklasse mit INotify oder Statisch entscheidet sich noch) welche durch Reflection alle Objekte sich schnappt als State speichert.
Da wir das ganze nicht aufblähen wollen, soll im Konstruktor eine maximale anzahl an States pro Objekt angegeben werden. Beim überschreiten wird der erste Stand überschrieben.
Das State wird ein serialisierter String oder ähnliches, damit der Vergleich einfacher ist.

Wenn Ihr noch anmerkungen habt, immer her damit

Stay tuned ;-)
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5985
Herkunft: Leipzig

Themenstarter:

beantworten | zitieren | melden

Hi Blacal,
das ist ja ein interessanter und ungewöhnlicher Ansatz. Bisher kannte ich nur Beispiele, wo ausschließlich das ViewModel überwacht wird.


Hi Ahrimaan,
das klingt gut, aber einfacher als es wahrscheinlich tatsächlich ist. Es reicht ja nicht, eine Basisklasse zur Verfügung zu stellen, um die Propertys der VM-Objekte zu überwachen. Man braucht auch eine Klasse, um Collections zu überwachen und eine UndoRedo-Command-Klasse und nicht zuletzt den UndoManager, der die unterschiedlichen Änderungen zu Aktionen zusammenfaßt.


Hi allerseits,
ich hab mich die letzten Tage sehr intensiv mit dem Thema auseinandergesetzt, und letztendlich laufen alle Ansätze darauf hinaus, daß es für jede Änderung am VM eine Methode gibt, die Änderung durchzuführen und eine Methode, die Änderung wieder rückgängig zu machen.


public class UndoRedoAction
{
  public Action DoAction { get; set; }
  public Action UndoAction { get; set; }
}

Der UndoManager sammelt und verwaltet diese Aktionen und führt die Methoden aus, je nachdem ob man einen Zustand herstellen oder rückgängig machen möchte. Das führt dazu, daß man für die Änderung eines Propertys in etwa folgendes schreiben müßte:


public bool SomeProperty
{
    get { return someProperty; }
    set
    {
        if (someProperty == value)
            return;

        bool oldValue = someProperty;
        UndoRedoAction action = new UndoRedoAction()
        {
            DoAction = (() => someProperty = value),
            UndoAction = (() => someProperty = oldValue)
        };

        UndoManager.Execute(action);
    }
}


Beim Ändern der Werte müßte natürlich jeweils noch das PropertyChanged-Ereignis aufgerufen werden, das hab ich aus Übersichtlichkeitsgründen weggelassen.

Der Trick ist nun, sich Schreibarbeit zu sparen und eine elegantere Möglichkeit zu finden. Dafür gibt es den Ansatz, im ViewModelBase eine SetValue-Methode zu implementieren, die genau das macht. Ein anderer Ansatz ist eine UndoRedoProperty-Klasse, die in etwa so verwendet wird, wie die DependencyPropertys in WPF und das eigentliche Property kapseln.

Was bei Property-Änderungen noch ganz gut funktioniert, ist bei Listen schon wesentlich komplizierter. Hier braucht man eine UndoRedoCollection-Klasse, die in der Lage ist, jede Zustandsänderung mit Hilfe der UndoRedoAction-Klasse zu tracken.

Ebenso braucht man für die Commands eine eigene Klasse, die auch einen Redo-Delegate unterstützt.

Bleibt noch der UndoRedoManager, der die einzelnen Änderungen zu Aktionen zusammenfaßt, die eigentlichen Änderungen durchführt und dafür sorgt, daß keine neuen Aktionen ausgelöst werden, während gerade ein Zustand rückgängig gemacht oder wiederhergestellt wird.

Alles in allem ist das ziemlich komplex, und es wird schwierig werden, eine vorhandene Anwendung damit "nachzurüsten". In anderen Fällen sollte man wohl besser auf den besagten Monitored Undo Framework oder Catel zurückgreifen.

Christian
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von MrSparkle am .
Weeks of programming can save you hours of planning
private Nachricht | Beiträge des Benutzers
Ahrimaan
myCSharp.de - Member



Dabei seit:
Beiträge: 363
Herkunft: Thorn

beantworten | zitieren | melden

Niemand hat gesagt, dass es einfach ist ;-)
Aber du hast recht, als Addon zu einem bestehendem Produkt fast nicht zu realisieren...
Ob es sich bei dem vorhandenen Frameworks lohnt, was eigenes zu machen sei dahingestellt..
private Nachricht | Beiträge des Benutzers