Ein Storyboard zum Anfassen:
Rapid Prototyping in der Softwareentwicklung

MVP. PoC und Prototyp – welche Unterschiede gibt es und worauf ist beim Rapid Prototyping zu achten?

Was haben das Storyboard für einen Zeichentrickfilm und ein Software-Prototyp gemeinsam?

Beides sind Visualisierung einer Idee und häufig auch der „rote Faden“ für die Umsetzung in einem Team.

Ein Grundkonzept des Design Thinking liegt u.a. darin alle Projektbeteiligen durch geeignete Visualisierungen in die Lage zu versetzen, „die gleiche Sprache zu sprechen“ und mögliche Lösungen für ein gegebenes Problem mithilfe verschiedener Techniken zu finden und zu bewerten.
Während dies bei einem Animationsfilm recht einfach dadurch gelingen kann, dass man z.B. einige Bilder des Storyboards zu einem Daumenkino kombiniert, um so z.B. die Bewegung von Personen und Objekten darstellen zu können, ist dies bei Software meist etwas aufwändiger.

Häufig werden im Rahmen der Weiterentwicklung bestehender Softwareprodukte (interaktive) Mockups oder Wireframes verwendet, um z.B. Übergänge von einem View auf einen anderen zu simulieren. Im Grunde ist dies aber auch nichts anderes als eine reine Visualisierungstechnik – und z.B. bei sogenannte Greenfield-Projekten nur bedingt hilfreich. Im Laufe der Zeit haben sich deshalb auch eine ganze Palette verschiedener Versionen des Prototypings entwickelt, die eines gemeinsam haben:

Durch einen überschaubaren Aufwand and (personellen) Ressourcen, Zeit oder Budget soll entweder

  • die Machbarkeit eines Ansatzes überprüft,
  • der zu erwartende Aufwand für die komplette Implementierung eines Projekts abgeschätzt oder
  • schnellstmöglich eine erste Version gebaut werden, mit der man sich Feedback (von Nutzern, Steakholdern, Projektbeteiligten) einholen kann.

Häufig werden die Begriffe Prototyp, Minimum Viable Product (MVP) und Proof of Concept (PoC) synonym verwendet, unterscheiden sich jedoch im Vergleich von einander:

  • Im Rahmen eines Proof of Concept wird in der Regel ein Teilbereich eines kompletten Produkts betrachtet. Dies kann bei einer datenbankgetriebenen Anwendung z.B. die Beispielimplementierung für die Verwendung einer bestimmten Datenbank sein oder für eine Webanwendung der Vergleich zwischen einem LAMP- und einem MEAN-Stack sein.
  • Ein klassicher Prototyp wird meistens dann entwickelt, wenn das Zusammenspiel verschiedener Technologien, Lösungsansätze etc. in einem kontrollierten Umfeld (z.B. einer Testumgebung) – in der Regel ohne echte Testnutzer – demonstriert werden soll.
  • Ein Minimum Viable Product ist einem Prototypen sehr ähnlich, im Regelfall jedoch so weit ausgereift, dass es – in begrenztem Rahmen, ohne dass bereits alle geplanten Funktionen zur Verfügung stehen – auch in einem produktiven Umfeld getestet werden kann.

Wie wird nun aber aus Prototyping Rapid Prototyping?

Die kurze Antwort: Indem man die Entwicklungszeit (für jede Iteration) des Prototypen möglichst kurz hält. Etwas ausführlicher bedeutet dies:

Viel hilft nicht unbedingt viel
Im Regelfall ist die Rechnung dass ein Job in der Hälfte der Zeit erledigt wird, wenn ich doppelt so viel Ressourcen zur Verfügung habe, falsch. Je größer das Team wird, umso mehr Overhead entsteht durch Absprachen und Selbstorganisation. Zudem besteht das Risiko, dass durch ein unterschiedliches Verständnis der Aufgaben-/ Fragestellung sich widersprechende Ansätze verwendet werden.

Wiederverwendbarkeit
In der Regel wird – weder beim Prototyping, noch der Entwicklung einer Softwarelösung – ausschließlich selbstentwickelter Code verwendet. Häufig kommen externe Bibliotheken, Tools und Frameworks zum Einsatz. Dabei sollte bedacht werden, dass

  • Sehr umfangreiche und große Frameworks meist auch eine längere Einarbeitungszeit benötigen. D.h. selbst wenn man schnell einen entsprechend qualifizierten Entwickler findet, ist es meist nicht ohne Weiteres möglich das Framework oder den Entwickler/ Dienstleister in einer späteren Projektphase zu wechseln.
  • Die verwendeten Frameworks und Bibliotheken sollten nicht nur möglichst viele schnell einsetzbare und Standardkomponenten enthalten, sondern die Komponenten sollten auch so gestaltet sein, dass ein Entwickler diese ohne viel Aufwand/ Code entsprechend der aktuellen Bedürfnisse modifizieren kann.
  • Low Code-Systeme erfreuen sich aktuell immer größerer Beliebtheit, da sie versprechen, dass im Grunde jeder Anwender – auch ohne Erfahrung in der Programmierung – seine eigenen Anwendungen entwickeln kann. Unabhängig davon, dass die meisten derartigen Systeme häufig nur Bausteine für von Ihnen selbst unterstützte Funktionen und Drittanwendungen bereitstellen, sollte man sich vor dem Einsatz dieser Systeme aber überlegen ob der Produktiveinsatz der damit einhergehenden Infrastrukturen überhaupt gewünscht oder sinnvoll ist. Ein typisches Beispiel dafür ist Power Automate der Firma Microsoft, das zwar sehr mächtig sein kann, jedoch auch die Nutzung der Azure-Cloudinfrastruktur voraussetzt.

Nichts überstürzen!
Auch wenn die reine Entwicklungszeit eines Prototypen, MVP oder PoC unter Optimalbedingungen recht zügig erfolgen kann, ist es gerade im Agilen Umfeld wichtig, dass damit nicht das Ende der Reise erreicht ist, sondern nicht nur Feedback eingeholt werden sollte, sondern dieses auch entsprechend in die Planung nächster Schritte oder weiterer Prototyping-Zyklen einfließen einfließen muss. Häufig dauert es mindestens genauso lange bis alle am Projekt beteiligten Mitglieder, Testnutzer, Mitglieder von Fokusgruppen die Möglichkeit hatten die entsprechenden Funktionen zu testen, ihre Rückmeldung dazu zu geben und diese auch entsprechend aufzuarbeiten.

Nachgefragt. Nicht-blockende Dialoge in WPF

Ein nicht-blockender Dialog, in .net, basierend auf dem MVVM-Designmuster. Geht das? Ja, klar!

Genau genommen, ist dies ein Post aus dem letzten Jahr – jedoch aktualisiert im Rahmen des Release meines BYTES.NET-Projekts. Der Quellcode der Beispielanwendung ist im zugehörigen Github-Repository zu finden und dient als Basis für diesen Artikel:

Jeder Entwickler kommt irgendwann an den Punkt, an dem er den Anwender nach weiteren Angaben oder Eingaben fragen muss. Sofern es sich nicht um eine „Single Page“- oder „inline“- Anwendung handelt, gehört es zum üblichen Vorgehen, einen Dialog zu öffnen, auf die Eingabe (und deren Bestätigung) zu warten und die Informationen anschließend (weiter) zu verarbeiten.

In einer Windows Forms-Anwendung gibt es den recht komfortablen OpenFileDialog um z.B. eine Datei zu öffnen. In Windows Presentation Foundation (WPF) gibt es diesen und ähnliche Dialoge standardmäßig nicht. Der übliche Tipp für WPF und den soeben genannten Anwendungsfall lautet daher häufig „bitte auf das System.Windows.Forms-Assembly referenzieren und dann den OpenFileDialog verwenden“.

Wie geht man aber damit um, wenn man eine Eingabe benötigt, für die es keinen vorgefertigten Dialog gibt oder wenn die Standarddialoge nicht ausreichen?

Entsprechend des MVVM-Designmusters, besteht ein Dialog aus

  • Einem Window, das als View verwendet wird und in dem die notwendigen Controls per XAML definiert werden.
  • Die Eigenschaften der Controls werden an die entsprechenden Eigenschaften eines View Models gebunden.
  • Anschließend wird der Dialog über Window.Show() anzeigen.

So weit so gut, jedoch liegt auch hier – wie allzu häufig – die Herausforderung im Detail. Was ist zum Beispiel…

  • … wenn der Dialog bzw. das Warten auf die Eingabe die übrigen Anwendungsfunktionen nicht blockieren darf (weil beispielsweise asynchrone Prozesse im Hintergrund ausgeführt werden)?
  • … wenn man häufig verschiedene Dialoge benötigt und die Codebase möglichst schlank halten möchte (z.B. auch vor dem Hintergrund des Bugfixings und der Wartbarkeit)?

Gleichzeitig ist es (aus meiner Sicht) deutlich eleganter – ähnlich wie im OpenFileDialog eine ShowDialog()-Methode über das View Model aufzurufen, anstatt eine neue Instanz der Window-Klasse zu erstellen und diese dann aufzurufen.

Die Lösung dafür bietet eine eigene DialogViewModel-Basisklasse, die von BYTES.NET.WPF.MVVM.ViewModel abgeleitet wird und somit auch INotifyPropertyChanged implementiert. Diese Klasse stellt eine Methode namens ShowDialog() bereit, in der gleich drei Dinge passieren:

  • Zum Ersten wird das zugehörige View (= Window) über die Methode ShowView() angezeigt.
  • Es wird darauf gewartet, dass der Anwender den View wieder schließt – entweder in Form einer Eingabebestätigung oder eines Abbruchs
  • Nach dem Schließen des View wird ein Boolean zurückgegeben, der in der aufrufenden Klasse dann ausgewertet werden kann:
Public Function ShowDialog() As Boolean

    'show the view
    ShowView()

    'wait for user input
    _myBlock = New EventWaitHandle(False, EventResetMode.ManualReset)
    WaitForEvent(_myBlock, New TimeSpan(24, 0, 0))

    'remove the wait handle
    _myBlock.Dispose()

    'return the dialog closing indicator
    Return Me.DialogResult

End Function

Ein Beispiel für eine daraus abgeleitete View Model-Klasse ist BYTES.NET.Sample.ViewModels.DialogVM auf Github. Durch die beiden überschreibbaren Methoden ShowView() und CloseView() ist es möglich nicht nur Windows als Views zu verwenden, sondern ggf. auch eigene GUI-Elemente wie z.B. Benutzersteuerelemente. Zudem lässt sich bei Bedarf noch während des Schließens der Wert von DialogResult manipulieren.

Kommen wir nun zum Verhindern des Blockens der Anwendung, beim Warten auf die Eingabebestätigung, WaitForEvent(). Nachdem ich einige (mehr oder minder erfolgreiche) Lösungsansätze probiert habe, bin ich in diesem Artikel auf eine sehr elegante Lösung zu dieser Problematik gestoßen: Die Grundidee beruht darauf einen zusätzlichen Thread zu verwenden um asynchron auf ein Ereignis zu warten – wodurch der Hauptthread (und mit ihm auch die Oberfläche) nicht blockiert wird. Zeitlich achgelagerte Operationen werden erst dann ausgeführt, wenn ShowDialog() eine Antwort zurückgegeben hat.

Private Function WaitForEvent(eventHandle As EventWaitHandle, Optional timeout As TimeSpan = Nothing) As Boolean

    Dim didWait As Boolean = False
    Dim frame = New DispatcherFrame()
    Dim start As New ParameterizedThreadStart(Sub()

                                                didWait = eventHandle.WaitOne(timeout)
                                                frame.[Continue] = False

                                              End Sub)

    Dim thread = New Thread(start)
    thread.Start()
    Dispatcher.PushFrame(frame)
    Return didWait

End Function

Schön und gut, was passiert nun aber, wenn der Anwender – statt auf z.B. einen Button zu drücken – das Dialogfenster über das kleine Kreuzchen rechts oben im Header schließt? Das Fenster wird zwar geschlossen, aber der „Warte“-Thread wird in der Regel nicht automatisch beendet und die Anwendung wird nicht sauber geschlossen.

Um auch dieses Problem zu lösen, gibt es einen recht einfachen Trick, den ich wiederum in diesem Artikel gefunden habe: Mithilfe eines Behaviors wird ein delegated Command (in meinem Fall ViewModelRelayCommand) an das ClosedEvent des Window gebunden, so dass – wie bei jedem anderen Command auch – die Close()-Methode (mit einem Negativwert für das DialogResult) aufgerufen werden kann. Als Teil eines XAML-definierten Window sieht das Ganze dann so aus:

<Window x:Class="Views.MVVM.DialogView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BYTES.NET.SAMPLE.Views.MVVM"
        
        xmlns:Behaviors="clr-namespace:BYTES.NET.WPF.Behaviors;assembly=BYTES.NET"
        Behaviors:WindowClosingBehavior.Closed="{Binding Path=Commands[CancelCmd]}"
        
        mc:Ignorable="d"
        Title="{Binding Title, Mode=OneWay}" Height="300" Width="500" ResizeMode="NoResize">

    ...

</Window>