Flow Design und Visualisierung (ein Ansatz)
Im letzten Beitrag habe ich über Visualisierung im Quellcode geschrieben. Natürlich ist das nicht das Ende von allen. Es geht immer besser. Aktuell gibt es aber noch kein Tool was mir wirklich für die Visualisierung von Flows geeignet erscheint. Idealerweise erfolgt nicht nur eine Visualisierung bestehender Komponenten sondern auch das Design. Im Ansatz und umständlich macht das der Sybase Powerdesigner z.B. mit seinem Komponenten Diagramm.
Welche Diagramm Typen sind den für eine Visualisierung geeignet? UML bietet da bereits einige Möglichkeiten. Mir sagen besonders das “Activity Diagramm” und das “BPMN Diagramm” zu. Das “Component Diagramm” ist naheliegend, übertrifft meiner Meinung jedoch nicht das “Activity Diagramm” für diesen Einsatzzweck. Die effizienteste Möglichkeit wäre die Umsetzung als Visual Studio Extension. Natürlich müssten der sich Quellcode und Visualisierung bei Änderungen gegenseitig aktualisieren.
Frei nach dieser Flow Design Notation sollte das Tool die Verbindungen zwischen den Klassen visualisieren und das Design ermöglich. Ein Beispiel. Wir ziehen eine Verbindung von einer Klasse zu einer anderen. Die Verbindung selbst benötigt nur einen Datentyp, Quelle und Ziel. Die Quelle ist ein Event der Klasse an welcher die Verbindung begonnen wurde. Das Ziele ist eine Funktion der Klasse zu welcher die Verbindung gezogen wurde. Ähnlich wie die Dialoge für die Fremdschlüssel bei Datenbanken müssten hier diese Angaben erfolgen. Für Quelle und Ziel könnte ein neuer Name angegeben oder ein Name mit passenden Datentyp ausgewählt werden. So würde alles zusammen passen. Eine Änderungen des Datentyps einer Verbindung hätte die Änderung aller abhängigen Events, Funktionen und weiteren Verbindungen zur Folge.
Das dies nur ein Ansatz ist um das gesamte Umfeld zu beschreiben ist mir klar. Als nächstes müssten die beliebige Schachtelung der Komponenten berücksichtigt werden. Der erwähnte Powerdesigner kann das nach einigen Zutun mit dem “Component Diagram” so darstellen.
Das wäre ein Beispiel für eine Schachtelung. Allerdings können Komponenten beliebig unter verschiedenen Aspekten verknüpft werden. Ein Diagramm wird nicht ausreichen, jedoch könnte jedes Diagramm im Codebehind die entsprechenden Verknüpfungen für den Aspekt vornehmen. Damit wäre wiederum eine Schachtelung nicht notwendig, sogar störend.
Die Beschränkung auf einzelne Aspekte stört mich nicht, ich finde sie hilfreich. Beispielsweise soll durch die Auswahl einer Kategorie eine Liste von Dokumenten geladen werden. Die beteiligten sind hier zwei ViewModels, ein Service und optional die Komponten zur asynchronen Ausführung.
Das einzige was die diese Komponente (LoadDocumentsAfterCategoryChanged) tun muss, ist die Verdrahtung vorzunehmen. Die beteiligten Komponenten müssen nichts voneinander wissen, jedoch dieser Komponente bekannt sein. Und hierbei fällt auf das Async und Sync nur innerhalb der Komponente einen Sinn machen. Im Diagramm müssten damit interne Komponenten und Abhängigkeiten unterschieden und entsprechend markiert werden. Markierung durch Attribute oder IDependency<T>, ich weiß es noch nicht.
Als nächstes fällt mir ein, das der DocumentService kritisch bezüglich des Events DocumentsLoaded ist. Es kann mehrere Abonnenten besitzen. Jedoch soll in diesem Fall nur das DocumentListViewModel informiert werden.
Async.Processing += DocumentService.LoadDocumentByCategory; // wird zu Async.Processing += c => DocumentService.LoadDocumentByCategory(c, Sync.Process)
DocumentService.DocumentLoaded += Sync.Process;
Wie könnte das denn dargestellt werden? Spontan habe ich hier keine gute Idee, vielleicht so?
An dieser Stelle unterbreche ich den Ideenfluss, was haben wir bis jetzt:
- Ein paar Ideen wie Visualisierung aussehen könnte
- Eine Vorstellung wie die Visualisierung in ein Projekt integriert werden kann
- Einige Regeln und viele neue Fragen
Und das Fazit:
Ich finde diese Visualisierung nützlich und effektiv, wenn sie mal ausgereift ist. Ein separates Tooling außerhalb der Entwicklungsumgebung ist mir zu wenig.
Profiler Trace in Tabelle konvertieren
Mal wieder was aus der Datenbank Ecke. Eine Anweisung konnte mir gerade helfen:
select * from fn_trace_gettable('c:\temp\profiler.trc', default)
Eingetragen in ein Abfragefenster des SQL Server Management Studio zeigt sie mir die Inhalte der Profiler Datei trotz nicht vorhandenem Sql Profiler. Der Return Value ist eine Tabelle, so konnte ich gleich die entsprechenden Filter anwenden.
Visualisierung von Abläufen, nicht im Quellcode
Im letzten Beitrag habe ich eine VS2010 Extension gezeigt, mit deren Hilfe sich Bilder als Dokumentationen in den Quellcode einfügen lassen. Sah gut aus, aber war es auch praktisch?
Inzwischen denke ich, Nein. Dokumentationen müssen manchmal auch angepasst werden. Und das war hier etwas umständlich. Dieses Vorgehen war zu teuer, nächster Versuch. Ziel ist es, die Dokumentation möglichst nahe am Quellcode zu haben, das ist so wie mit den Tests. Also vielleicht nur Teil des Projektes (die Solution ist schon zu weit weg). Wir können dort beliebige Dateien hinzufügen, das sieht dann beispielsweise so aus.
Hier haben wir folgende Vorteile. Wir können die Dokumentation anpassen. Wir können sie anzeigen, wenn wir das möchten und stören nicht den Lesefluss im Quellcode. Einen eigenen Projektordner würde ich in den meisten Fällen trotzdem nicht anlegen, sondern immer nach “möglichst nah am Quellcode” vorgehen. Hier als Beispiel mit dem Namen der Quelldatei welche dokumentiert wird. Das reicht aber nicht. Als nächstes sollte der Standardeditor für das Dokument angepasst werden.
Wichtig ist hier, “Als Standard” auswählen. Jetzt noch eine Extension finden, welche den “DependentUpon” Eintrag in der Projektdatei anpassen kann. Für diesen Zweck habe ich nach kurzer Suche die Extension NestIn gefunden. Die tut genau das.
Noch besser wäre ein integrierter Editor mit dem man tatsächlich arbeiten kann. Ich habe noch keinen gefunden, und nehme gerne Vorschläge entgegen. Anforderungen kommen womöglich in einem folgendem Beitrag.
Visualisierung von Abläufen im Quellcode
Gerade bei komplexen Verknüpfungen zwischen Komponenten, ist eine Visualisierung manchmal hilfreich. Jetzt erst eine Dokumentation zu durchsuchen ist aufwendig. Schöner wäre, wenn die Dokumentation direkt im Quelltext platziert werden könnten. Anhand der Game Klasse des TicTacToe Quellcodes kann das dann so aussehen.
Das finde ich gut, denn es gibt wesentlich komplexere Komponenten. Möglich macht dies die VS2010 Extension Image Insertion aus der “Visual Studio Gallerie”.
Eingaben für SecureString
Vor einiger Zeit hatte ich die Eingabemöglichkeiten für SecureString bemängelt, dass möchte ich nun mal schnell korrigieren. In WPF unterstützt die PasswordBox die Eingabe von SecureStrings seit Framework 3.5.
Auf der Console müssen (nach aktuellem Kenntnisstand) weiterhin eigene Implementierungen verwendet werden. Im Vergleich zu http://davidhayden.com/blog/dave/archive/2006/03/04/2873.aspx, http://www.guidanceshare.com/wiki/How_To_Use_SecureString_in_.NET_2.0 oder weiteren Lösungen mit WHILE finde ich, dass FOR unterbewertet ist. Die hier gezeigte Lösung verdeckt die Ausgaben vollständig und erlaubt die Neueingabe bei Tippfehlern.
private static SecureString GetPassword() { var password = new SecureString(); for (char key = (char)8; key != (char)13; key = Console.ReadKey(true).KeyChar) { if (key == (char)8) password.Clear(); else password.AppendChar(key); } return password; }
Tic Tac Toe – event based
Nachdem hier schon wieder einige Zeit nichts veröffentlicht wurde, kommt nun wieder ein umfangreicherer Beitrag. Ich verstehe auch nicht wirklich woher andere Blog Schreiber die Zeit nehmen. Ich habe jedenfalls immer einiges zu tun, was mir wichtiger erscheint. In der letzten Zeit habe ich mich eingehender mit Event based components beschäftigt, und ich fange an sie sehr zu mögen. Inzwischen verwende ich diese Art der Implementierung in allen Projekten. Was meine Vorliebe zu MEF angeht, sie ist noch da aber ich verwende sie nur sehr sparsam in Verbindung mit EBC. Kommen wir zur Tic Tac Toe Implementierung.
Ich habe einige Implementierungen im Internet gefunden und auch frühere Versionen eigener Implementierungen angesehen. Ich bin mir sicher es geht besser, und ich versuche das heute mit EBC. Nach einigen Projekten erscheint es mir der folgende Ablauf sinnvoll :
- Konzept erstellen, und wenn es nur ein paar Skizzen auf Papier sind
- Implementierung der Datenobjekte
- Implementierung der Komponentenschnittstellen
- Implementierung der Tests gegen die Komponenten
- Ausbau der Komponenten und Verdrahtung
Kommen wir zum Konzept, eine einfache Skizze reicht. Etwas Erfahrung benötigt man trotzdem, sonst landen einige Entwürfe im Papierkorb. Ich erspare mir dadurch einiges an Ärger, die Zeit lohnt sich also. Leider habe ich noch kein gutes Tool gefunden mit dem die Visualisierung einfach und schnell möglich ist.
Die Player sind vorerst nicht so wichtig, die bekommen für die Ermittlung einen Zuges den aktuellen Status und geben dafür ein Feld zurück das gesetzt werden soll. Den Spieler auszuwählen übernimmt ein Sequencer anhand des Status. Ich fange hier aber mit dem Game, der eigentlichen Logik an. Zunächst benötige ich jedoch meine Datenklassen. Einige wurden bereits genannt, wir haben nun Board, Field, Figure, Mode und State.
public class State { /// <summary> /// Initialisieren /// </summary> public State() { Board = new Board(); CurrentMode = Mode.Running; } /// <summary> /// Spielfeld, Aktueller Zustand /// </summary> public Board Board { get; private set; } /// <summary> /// Aktueller Spielstatus /// </summary> public Mode CurrentMode { get; internal set; } /// <summary> /// Aktueller Spieler /// </summary> public Figure? CurrentFigure { get { if (CurrentMode != Mode.Running) return null; return Board.Moves % 2 == 0 ? Figure.Cross : Figure.Circle; } } /// <summary> /// Gewinner des Spiels /// </summary> public Figure? Winner { get { if (CurrentMode != Mode.Winning) return null; return Board.Moves % 2 == 0 ? Figure.Circle : Figure.Cross; } } }
Der Spielstatus enthält alle Informationen um einen Spieler die Berechnung eines Zuges und einem UI die Anzeige zu ermöglichen. Hier ist auch etwas Logik enthalten, jedoch datenbezogen. Der aktuelle Spieler oder Gewinner wird aus der Zuganzahl und dem Mode ermittelt
public enum Mode { Running, Winning, Ended }
Der Mode ist wohl selbserklärend, ebenso wie die Figure Klasse. Figure wird als Nullable verwendet, auf ein None wurde hier bewusst verzichtet.
public enum Figure { Cross, Circle }
Die Felder des Board werden auf den Nummernblock für manuelle Spieler abgebildet.
public enum Field { Num7, Num8, Num9, Num4, Num5, Num6, Num1, Num2, Num3 }
Für das Board verwende ich vorerst ein Dictionary. Ein Array hätte es mit angepasster Logik auch getan. Ich hatte eine Lösung gefunden in welcher ein String verwendet wurde um darauf Regular Expressions anzuwenden um einen Gewinner zu ermitteln; sehr interessant. Die Auskommentierung verhindert das Überschreiben eines bereits belegten Feldes.
public class Board { private Dictionary<Field, Figure> data = new Dictionary<Field, Figure>(); public Figure? this[Field field] { get { if (data.ContainsKey(field)) return data[field]; return null; } internal set { if (!value.HasValue) return; if (!data.ContainsKey(field)) data.Add(field, value.Value); //else // data[field] = value.Value; } } public int Moves { get { return data.Count; } } }
Damit können wir mit Punkt 3. weitermachen, den ich hier mit Punkt 5. verbinde. Mit welcher Klassen anfangen? Das ist eigentlich egal, weil zunächst nur die Schnittstellen definiert werden. Was passiert wenn der Spieler ein Feld setzt? Wir müssen prüfen ob der Zug ausgeführt werden kann und müssen diesen auch ausführen, wenn möglich. Hier haben wir wenig zu tun, das Board nimmt uns die Arbeit bereits ab, es setzt nur mögliche Züge. Und der aktuelle Spieler für den nächsten Zug wird aus der Zuganzahl bestimmt, also wäre der Spieler bei einem falschen Zug nochmals an der Reihe.
class Moving { public void MoveStarted(State state, Field field) { state.Board[field] = state.CurrentFigure; MoveEnded(state); } public event Action<State> MoveEnded = delegate { }; }
Die Winning Klasse übernimmt die Erkennung ob das Spiel gewonnen wurde oder unentschieden ausgegangen ist. Das Spiel ist als unentschieden definiert, wenn alle Felder belegt wurden und keine Gewinnposition ermittelt wurde. Der Statuswechsel für Mode ist hier definiert.
public class Winning { public void Detect(State state) { if (state.Board.Moves == 9) state.CurrentMode = Mode.Ended; if (IsWinning(state.Board)) state.CurrentMode = Mode.Winning; Changed(state); } public event Action<State> Changed = delegate { }; private bool IsWinning(Board board) { return SameFigure(board, Field.Num1, Field.Num2, Field.Num3) || SameFigure(board, Field.Num4, Field.Num5, Field.Num6) || SameFigure(board, Field.Num7, Field.Num8, Field.Num9) || SameFigure(board, Field.Num1, Field.Num4, Field.Num7) || SameFigure(board, Field.Num2, Field.Num5, Field.Num8) || SameFigure(board, Field.Num3, Field.Num6, Field.Num9) || SameFigure(board, Field.Num1, Field.Num5, Field.Num9) || SameFigure(board, Field.Num7, Field.Num5, Field.Num3); } private bool SameFigure(Board board, Field f1, Field f2, Field f3) { var fig1 = board[f1]; var fig2 = board[f2]; var fig3 = board[f3]; return fig1.HasValue && fig2.HasValue && fig3.HasValue && fig1 == fig2 && fig2 == fig3; } }
Die Gewinnermittlung ist wie das Board sicher eleganter zu lösen, hier geht es aber vorrangig um EBC, wovon wir bisher nur wenig gesehen haben. Die Verdrahtung und Verwendung der bisher gezeigten Klassen erfolgt in der Klasse Game. Aber auch hier ist nicht viel zu tun.
public class Game { private Join<State, Field> join; private Winning winning; private Moving move; public Game () { join = new Join<State, Field>(); winning = new Winning(); move = new Moving(); join.OnCompleted += move.MoveStarted; move.MoveEnded += winning.Detect; winning.Changed += Changed; } private void Changed(State state) { join.InputA(state); StateChanged(state); } /// <summary> /// Neues Spiel beginnen /// </summary> public void Start() { Changed(new State()); } /// <summary> /// Spielzug ausführen /// </summary> public void Move(Field field) { join.InputB(field); } /// <summary> /// Spielstatus, Aufforderung für Spielzug /// </summary> public event Action<State> StateChanged = delegate { }; }
Es erfolgt die Verdrahtung nach dem zuvor gezeigten Konzept. Hier wird eine Vereinfachung, der aus dem Tooling oder anderen Quellen bekannten Join Klasse verwendet um auf den Zug eines Spielers zu warten. Zunächst sollte Start aufgerufen werden um das Spiel zu beginnen, dabei wird ein neuer Status erzeugt und bereitgestellt. Jetzt wartet Join auf den Zug eines Spielers der mit Move vorgenommen werden kann. Mit dem Zug wird die Moving und Winning Komponenten durchlaufen. Der geänderte Spielstatus wird wie beim Start weiter verwendet. Das war es auch schon… der Sequencer vielleicht noch
/// <summary> /// Spielablauf, benachichtigt den jeweiligen Spieler für einen Zug /// </summary> public class Sequencer { /// <summary> /// Figure.Cross ist am Zug /// </summary> public event Action<State> MovingCross; /// <summary> /// Figure.Circle ist am Zug /// </summary> public event Action<State> MovingCircle; /// <summary> /// Spieler auswählen um eine Figur zu setzen, solange das Spiel läuft /// </summary> public void MakeMove(State state) { if (state.CurrentMode != Mode.Running) return; if (state.CurrentFigure == Figure.Circle) MovingCircle(state); else MovingCross(state); } }
Der tut auch nicht mehr als eine Verteilung auf verschiedene Spieler vorzunehmen. Der Spieler von der Schnittstelle her ebenso einfach. Der Spieler könnte eine geklonte Instanz der State Klasse erhalten, aktuell ist sind die Eigenschaften außerhalb nur lesbar, somit ist alles in Ordnung.
public interface IPlayer { void MakeMove(State state); event Action<Field> Moving; string Name { get; } }
- Alles in Ordnung kann man auch erst nach mehreren Tests sagen, also liefere ich eine Auswahl gleich mit.
[TestClass] public class GameTests { [TestMethod] public void Spieler_X_hat_den_ersten_Zug() { var target = new State(); var game = new Game(); game.StateChanged += t => target = t; game.Start(); Assert.AreEqual(Mode.Running, target.CurrentMode); Assert.AreEqual(Figure.Cross, target.CurrentFigure); } [TestMethod] public void Spieler_ziehen_abwechselnd() { var target = new State(); var game = new Game(); game.StateChanged += t => target = t; game.Start(); game.Move(Field.Num5); Assert.AreEqual(Figure.Circle, target.CurrentFigure); } [TestMethod] public void Spieler_ist_bei_ungueltigem_Zug_nochmal_dran() { var target = new State(); var game = new Game(); game.StateChanged += t => target = t; game.Start(); game.Move(Field.Num5); game.Move(Field.Num5); Assert.AreEqual(Figure.Circle, target.CurrentFigure); } [TestMethod] public void Ein_Spieler_kann_gewinnen() { // X X X // O O // var target = new State(); var game = new Game(); game.StateChanged += t => target = t; game.Start(); game.Move(Field.Num7); game.Move(Field.Num4); game.Move(Field.Num8); game.Move(Field.Num5); game.Move(Field.Num9); Assert.AreEqual(Mode.Winning, target.CurrentMode); Assert.AreEqual(null, target.CurrentFigure); Assert.AreEqual(Figure.Cross, target.Winner); } [TestMethod] public void Spiel_kann_unentschieden_ausgehen() { // X O X // X O X // O X O var target = new State(); var game = new Game(); game.StateChanged += t => target = t; game.Start(); game.Move(Field.Num7); game.Move(Field.Num8); game.Move(Field.Num9); game.Move(Field.Num5); game.Move(Field.Num4); game.Move(Field.Num1); game.Move(Field.Num2); game.Move(Field.Num6); game.Move(Field.Num3); Assert.AreEqual(Mode.Ended, target.CurrentMode); Assert.AreEqual(null, target.CurrentFigure); Assert.AreEqual(null, target.Winner); } }
In einem nächsten Posting werde ich mit um einen Simulator kümmern (Tic Tac Toe Arena) in welchem verschiedenen Computerspieler gegeneinander antreten können. Auch einige Optimierungen und ein UI wäre nicht schlecht, mal sehen was meine Zeit zulässt.
Abortable Async Pattern für EBC
Ralf Westphal hat in Asynchrone Kommunikation mit EBCs statt “Async-Pattern” einen kleinen Codeschnipsel zur asynchronen Ausführung von Funktionen veröffentlicht. Der funktioniert auch sehr gut. Wie genau alle Komponenten verknüpft werden kann im oben stehenden verweis nachgelesen werden.
public class EventAsynchronizer<T> { public void Process(T msg) { ThreadPool.QueueUserWorkItem(x => this.OnProcessing(msg), null); } public event Action<T> OnProcessing; }
Was passiert aber, wenn der Anwender teil der Kommunikation wird. Ein Beispiel. Wir haben ein Benutzerformular in welchem in einer Liste A ein Eintrag A1 ausgewählt wird. Mit der Auswahl wird die Ansicht einer Liste B mit Eintrag A1 als Parameter aktualisiert. Die Aktualisierung erfolgt nun aber asynchron. Während Liste B noch aktualisiert wird wählt der Anwender den Eintrag A2 aus Liste A. Wieder startet eine asynchrone Anfrage um Liste B zu aktualisieren, dieses mal mit Parameter A2. Man kann das Spiel noch weiterführen und mit Aktualisierung der Liste B eine Aktualisierung einer Liste C ausführen (wie in einem mir vorliegenden praktischen Anwendungsfall). Abgesehen davon ist es nicht zweckmäßig die Liste B für A1 zu aktualisieren, während schon eine Aktualisierung vor A2 angefordert wurde.
Das Problem liegt woanders. Was passiert wenn die Abfrage der Daten für Liste B durch Eintrag A2 schneller erfolgt als die Abfrage durch Eintrag A1. Die Liste B wird am Ende die falschen Werte anzeigen. Den in Liste A ist der Eintrag A2 ausgewählt, aber die Liste B wurde für den Eintrag A1 zuletzt aktualisiert. Somit passen die Werte nicht zusammen.
Die Lösung liegt für mich in einer kleinen Änderung der oben gezeigten Komponente, der Rest bleibt so (hier ein kleines Hoch auf Event Based Components). In Grundzügen kann die Komponente wie folgt umgesetzt werden.
public class EventAbortableThread<T> { private Thread thread = null; public void Process(T msg) { if (thread != null) { thread.Abort(); thread = null; } thread = new Thread(new ParameterizedThreadStart(Processing)); thread.Start(msg); } private void Processing(object msg) { OnProcessing((T)msg); } public event Action<T> OnProcessing; }
Statt dem Threadpool wird jetzt ein einfacher Thread verwendet, welcher bei Bedarf abgebrochen wird. Auf diese Weise hat sich die Anwendung tatsächlich mit schnelleren Reaktionszeiten bei Benutzerinteraktionen bedankt. Wichtiger ist, es kommt nicht mehr zu falschen Anzeigen.
Der Benutzer wählt wiederum den Eintrag A1 aus, die Daten für die Liste B werden abgerufen. Der Benutzer wartet nicht und wählt den Eintrag A2 aus. Jetzt wird das Laden der Daten für Eintrag A1 abgebrochen. Statt dessen wird wird das Laden der Daten für Eintrag A2 angewiesen, welche kurz darauf in der Liste B angezeigt werden. Was fehlt noch, vielleicht noch ein kleiner Test der das Verhalten prüft und dokumentiert.
[TestClass] public class EventAbortableThreadTests { [TestMethod] public void Nur_die_letzte_Anfrage_wird_verarbeitet() { int target = 0; var thread = new EventAbortableThread<int>(); thread.OnProcessing += (wait) => { Thread.Sleep(wait); target += wait; }; thread.Process(800); thread.Process(400); thread.Process(200); Assert.AreEqual(0, target, "Ergebnis noch nicht erwartet"); Thread.Sleep(1000); Assert.AreEqual(200, target, "Unerwartetes Ergebnis"); } }
Im Test wird die Komponente in kurzen Abständen mit 3 unterschiedlichen Angaben zur Prozessdauer aufgerufen. Mit der ursprünglichen Komponente würde am Ende target mit dem Wert 1400 belegt sein. Hier jedoch nicht, die letzte Anforderung war der Wert 200, die vorherigen Abfragen erzeugen keine Rückantwort.
Anwendungsbeispiel für asynchrone Komponenten
Gestern hatte ich eine Möglichkeit gezeigt, mit der man auf einfache Weise Komponenten asynchron machen kann und diese im Anschluss event based zu verwenden. Heute möchte ich das Beispiel erweitern und etwas näher in die Praxis rücken. Zunächst verwende ich keine Standardtypen mehr sondern definiere mir eine Datenklasse.
public class Item { public Item() { Id = Guid.NewGuid().ToString(); } public string Id { get; set; } public override string ToString() { return Id; } public static IEnumerable<Item> Get(int count) { List<Item> items = new List<Item>(); for (int i = 0; i < count; i++) { items.Add(new Item()); } return items; } }
Ich möchte Instanzen dieser Klasse asynchron in einem Repository speichern und jeweils benachrichtigt werden, wenn ein Element gespeichert wurde. Das Repository ist hier einfach.
class RepositoryService { public void SaveItem(Item item) { Console.WriteLine("Save : {0}", item); Thread.Sleep(50); } }
Ich will auch wissen, welches Item gespeichert wurde. Ich kann eine Funktion definieren und den Rückgabewert entsprechend setzen. Jetzt verwende ich aber nur mal Actions. In ProcessAsync sorge ich dafür, dass der Parameter der Funktion Process in OnCompleted wieder verwendet wird. Außerdem wird der SynchrinzationContext für die problemlose Verwendung in einer UI verwendet.
public class ProcessAsync<P> { private Action<P> action; private readonly SynchronizationContext ctx = SynchronizationContext.Current; public ProcessAsync(Action<P> action) { this.action = action; } public void Process(P parameter) { action.BeginInvoke(parameter, new AsyncCallback(WorkCallback), parameter); } public event Action<P> OnCompleted = (s) => {}; private void WorkCallback(IAsyncResult result) { var action = (Action<P>)((AsyncResult)result).AsyncDelegate; var param = (P)((AsyncResult)result).AsyncState; action.EndInvoke(result); if (ctx != null) ctx.Send(x => OnCompleted(param), null); else OnCompleted(param); } }
Dann leite ich von ProcessAsync ab und definiere mir damit die Funktionalität um in das Repository zu schreiben. Das geht jetzt sehr schnell. Alternativ kann ich ProcessAsync auch innerhalb einer Klasse instanzieren und verwenden.
class SaveItems : ProcessAsync<Item> { public SaveItems(RepositoryService service) : base( (item) => service.SaveItem(item)) {} }
Und damit sind wir schon fast am Ende und gehen zur Hauptroutine. Hier wird die Funktion zum Speichern instanziiert und das gewünschte Repository übergeben. Wir verbinden OnCompleted mit der Ausgabe und arbeiten 10 generierte Items ab.
class Program { static void Main(string[] args) { var p = new SaveItems(new RepositoryService()); p.OnCompleted += (s) => Console.WriteLine("Saved : {0}", s); foreach (var item in Item.Get(10)) { p.Process(new Item()); } Console.ReadLine(); } }
Als Ergebnis erhalten wird die folgende Ausgabe und sehen recht gut die asynchrone Verarbeitung. Am besten gefällt mir bei dieser Lösung das die asynchrone Verarbeitung von der Logik getrennt werden kann, wie in SaveItems gezeigt. SaveItems definiert nur die Funktionalität in das injizierte Repository zu speichern. ProcessItems definiert nur die Funktionalität beliebige Actions asynchron auszuführen. Die Items sind was sie sein sollen, eben nur Daten.
Asynchrone Komponenten
Seit einiger Zeit beschäftige ich mich mit Event Based Components. Klar, ich verwende diese auch in Projekten. Kürzlich empfand ich die einmal vorgeschlagene Möglichkeit, wahlweise asynchrone Verarbeitung zu ermöglichen, indem jeweils eine Komponente vor- bzw. nachgeschalten wird, nicht passend. Es sollten mehrere Komponenten miteinander verdrahtet werden und dabei asynchron funktionieren. Ich habe dafür eine einfache Möglichkeit verwendet, Delegates. Wie passend bei EBC…
Fangen wir mit einer Komponente an, die synchron ausgeführt wird. Zum warmwerden sozusagen. Nur als Ausgangspunkt, Erklärungen sind wohl nicht notwendig.
public void Process(string arg) { Thread.Sleep(1000); OnResult(arg); } public event Action<string> OnResult = (s) => {};
Was müssen wir tun um diese Komponente asynchon zu implementieren. Zunächst benötigen wir für diese Komponente ein delegate dessen Signatur sich aus dem event OnResult und der Funktion Process zusammensetzt. Ich habe das delegate mal WorkEvent genannt, und dazu die passende Methode Work implementiert. Zusammen wird das in der Methode Process instanziert und asnychron aufgerufen. Dazu wird noch die Callback Funktion benötigt, die hier WorkCallback genannt wurde. Auch hier ist die Implementierung einfach, wir holen uns mit EndInvoke das Ergebnis und senden es an wen auch immer.
class Async { public void Process(string arg) { var work = new WorkEvent(Work); work.BeginInvoke(arg, new AsyncCallback(WorkCallback), null); } public event Action<string> OnResult = (s) => { }; private delegate string WorkEvent(string arg); private void WorkCallback(IAsyncResult result) { var work = (WorkEvent)((AsyncResult)result).AsyncDelegate; var value = work.EndInvoke(result); OnResult(value); } private string Work(string arg) { Thread.Sleep(1000); return arg; } }
Das Verhalten muss jedoch immer wieder implementiert werden, wenn es gewünscht wird. Also belassen wir es nicht dabei und probieren mal eine generische Lösung. Anstelle der Work Methode könnten wir beliebige Funktionen ausführen. Auch das delegate wird nicht mehr benötigt. Die Funktion kann im Konstruktor, der Methode Process oder als Eigenschaft injiziert werden.
public class CalculateAsync<In, Out> { public void Process(Func<In, In, Out> func, In in1, In in2) { func.BeginInvoke(in1, in2, new AsyncCallback(WorkCallback), null); } public event Action<Out> OnResult = (s) => { }; private void WorkCallback(IAsyncResult result) { var work = (Func<In, In, Out>)((AsyncResult)result).AsyncDelegate; var value = work.EndInvoke(result); OnResult(value); } }
Als Verwendungsbeispiel zeige ich einen kleinen Rechner
var calculate = new CalculateAsync<int, int>(); calculate.OnResult += Console.WriteLine; calculate.Process((a, b) => a + b, 2, 2); calculate.Process((a, b) => a * b, 2, 2); calculate.Process((a, b) => a - b, 2, 2); calculate.Process((a, b) => a / b, 2, 2);
Würde die Funktion im Konstruktor übergeben könnte man andere Beispiele implementieren.
var add = new CalculateAsync<int, int>((a, b) => a + b); var mul = new CalculateAsync<int, int>((a, b) => a * b); add.OnResult += r => mul.Process(r, 3); mul.OnResult += Console.WriteLine; add.Process(1, 2);
In der Realität sind solche Komponenten sinnvoll um Aktionen mit oder ohne Parametern auszuführen und um nach deren Beendigung ein Ergebnis zu erhalten. Wir können beispielsweise die Parameter einer Datenbankabfrage übergeben und asynchron auf das Ergebnis warten, ohne Verwendung von OnResult arbeiten wir OneWay. Ich will sagen, wir sind flexibel und auf einfachste Weise asynchron.
Zeitraffer
Grandioses Video, Dresden im Zeitraffer. Mir kommt es vor als hätte ich den letzten Blogeintrag erst vor wenigen Tage geschrieben. Und dann “Das Ende der Sommerzeit”. Und ich habe nicht mal gute Vorsätze für das neue Jahr, aber einfach immer was zu tun.