Vor ein paar Tagen hatte ich ja von meinen ersten Gehversuchen im Bereich von WPF & MVVM berichtet.
Nun hat mich ein Kumpel, der genau dieses Feld schon eine ganze Weile beackert, vor eine Herausforderung gestellt:
a) Meine GUI sollte ansprechbar (“responsive”) bleiben, während der Merge ausgeführt wird.
b) Es soll nicht immer die ganze GUI nach jeder Aktion evaluiert werden.
Diese zwei Herausforderungen haben ganz realistische Hintergünde. Der Benutzer mag es nicht, wenn er das Gefühl hat, das Program hätte sich aufgehängt. Deshalb sollte man eine Oberfläche schreiben, die dem Benutzer mitteilt: “Nein, ich bin nicht abgestürzt, aber ich mach grad’ noch was!”.
Und das zweite ist etwas, was bei kleinen Demo-Apps wie dieser nie auffällt, aber bei größeren GUIs schnell zum Problem wird: Das Evaluieren jedes Controls nach jeder Aktion kann ziemlich in die Zeit gehen und Performance fressen.
Beides lässt sich unter dem Kürzel UX für “User eXperience” subsummieren.
OK, Aufgabe a) zu lösen ist mit der vom .Net Framework 4.5 mitgebrachten Tasks-Library relativ banal. Und selbst vorher hätte man das halt mit einem simplen BackgroundWorker-Thread gelöst.
Sobald man aber eine Aufgabe in einen Hintergrund-Thread verlagert hat, man plötzlich ein Problem, dass es so vorher nicht gab: Der Benutzer kann plötzlich an der Oberfläche herumklicken und zum Beispiel den gleichen Button zum zweiten Mal drücken, bevor die erste Aktion beendet ist.
Hier muss man sich jetzt also plötzlich Gedanken machen, was genau der Benutzer eigentlich machen darf, während der Task läuft. In meinem Beispiel ist das zum Glück relativ simpel: Nichts. Die UI soll halt lediglich nicht eingefroren erscheinen, während er den Task erledigt.
Um den Task künstlich zu verlängern, habe ich ins Model einfach mal einen Sleep von 5 Sekunden eingebaut, damit man auch mal was sieht. Ansonsten wurde das Model im Vergleich zur Vorgänger-Implementierung nicht angepasst.
In der wieder unten verlinkten ZIP-Datei findet ihr die Umstellung auf einen Task in MVVM_responsive.
Entgegen vieler Beispiele, die man im Internet findet, sollte man jedoch nicht Task.Factory.StartNew() verwenden, sondern meistens einfach nur Task.Run(), wenn man nicth ganz gute Gründe hat. Hier ist das schön erklärt.
Was natürlich auch zu beachten ist, ist dass wir jetzt Multi-Threading betreiben, wir unsere Rückgriffe auf die UI also sorgfältig kapseln sollten. Dankenswerterweise haben wir die Zugriffe alle schon wegge-interface-t gehabt. Siehe dazu auch die Anpassung in UIServicesWPF.cs.
Jetzt zum Teil b) der Aufgabe.
Wenn man sich vom globalen CommandManager loslöst, ist man wieder selbst verantwortlich dafür, Abhängigkeiten zwischen Anzeigen und Zuständen von Controls zu ermitteln und ggf. zu aktualisieren.
So sollen zum Beispiel meine Merge-Buttons nur aktiv sein, wenn die Textboxen abgefüllt sind. D.h. der Button.Enabled Zustand hängt davon ab, wie sich der Zustand von TextBox.Text verändert.
In MVVM_NoRequery und MVVM_NoRequery_alternate habe ich mal zwei Wege aufgezeigt.
Die Komplexität des ganzen hängt natürlich immer davon ab, wie sehr die einzelnen Controls voneinander abhäng sind.
Die Lösung, wie man es für größere und/oder komplexere ViewModels macht, ist bei StackOverflow skizziert.