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>

Schnell mal eben gebastelt:
Rapid Prototyping in der Softwareentwicklung

(Software-)Prototypen bieten eine gute Möglichkeit Ideen und Lösungsansätze schnell und effizient zu testen. Aber was sollte man dabei zu beachten?

Im Rahmen der (Weiter-)Entwicklung einer Software ist es nichts Neues oder Ungewöhnliches Prototypen einer Anwendung oder der – ggf. zu verbessernden – Teilfunktion(en) zu erstellen. Während in der eher traditionellen Softwareentwicklung häufig sogenannte Minimum Viable Product(s) (MVP) entstehen, kommt dem Rapid Prototyping in der agilen Softwareentwicklung – häufig im Rahmen des sogenannten Design Thinking – eine besondere Bedeutung zu:

Im Unterschied zu einem MVP, das häufig gegen Ende einer Konzept- oder Anforderungsanalysephase und mit teils doch recht hohen Entwicklungsaufwänden ensteht, soll ein Rapid Prototyp mit möglichst geringem Entwicklungsaufwand und auch bereits möglichst frühzeitig erstellt werden. Häufig werden für einzelne Ideen dedizierte Prototypen gebaut oder spezielle Lösungsansätze werden mithilfe von Prototypen validiert – um das so erhaltene Feedback frühestmöglich bei der Projektplanung zu berücksichtigen.

Häufig werden die Prototypen gemeinsam mit Mockups oder Wireframes verwendet um die Gesamtarchitektur einer Lösung zu visualisieren. Im Unterschied dazu, sollen Protypen es Teammitgliedern oder ausgewählten Testnutzern gestatten typische Anwendungsfälle auszuprobieren oder eingesetzte Technologien (z.B. auf Performance) konkret zu bewerten.

Was ist jedoch beim Einsatz von Rapid Prototyping zusätzlich zu beachten?

Keep it simple!

Wie schon erwähnt, soll ein Prototyp meist einen ganz bestimmten Ansatz oder eine Idee abbilden. Schnell passiert es jedoch, dass man sich z.B. aufgrund der verwendeten Technologien – oder aus dem Wunsch heraus einmal investierte Zeit sinnvoll weiterverwenden zu können – im Detail verliert oder unnötig komplizierte Architekturen implementiert. Wenn es die (Gesamt-)Anwendungsarchitektur zulässt modulare Feature-Prototypen zu erstellen, optimal. Falls nicht, bleiben Sie fokussiert und konzentrieren Sie sich auf die eigentliche Grundidee! Was ist der minimale Funktionsumfang der nötig ist um den aktuellen Ansatz testen zu können?

Es sollte darauf geachtet werden, dass die Frage-/ Aufgabenstellung realistisch ist bzw. so weit heruntergebrochen werden kann, dass sie mit einem einfachen Prototypen abgebildet werden kann. Während einfache Datenkonvertierungen und Statusübergänge u.U. noch abbildbar sind, macht es wenig Sinn zu versuchen komplette Workflows in einem einzelnen Prototypen abzubilden.

Low-Code?

Ein Artikel zum Thema Rapid Prototyping aus dem letzten Jahr legt u.a. einen der Schwerpunkte auf Low-Code-Plattformen. Es wird beschrieben, dass die Einsatzmöglichkeit von Low-Coding-Systemen u.a. von Ansatz, Funktionsumfang, Zielgruppe und der Bereitschaft der Mitarbeiter abhängt, sich mit der Erstellung von Applikationen zu beschäftigen.

Grundsätzlich möchte ich dieser Aussage nicht wiederprechen, finde jedoch, dass man an dieser Stelle zwischen „Prototypen zur Gestaltung von (Geschäfts-)Prozessen“ und „echten Anwendungsprototypen“ unterscheiden sollte: Ich glaube durchaus, dass man Prototypen z.B. für die Modellierung von Prozessen innerhalb integrierter Geschäftsanwendungen per Low-Code-Verfahren erstellen kann. Für „native“ Software hängt dies stark von den verwendeten Technologien ab – und führt fast immer dazu, dass zumindest im geringfügigen Umfang Programmierung notwendig ist.

Häufig kann man sich das Leben durch den gezielten und bewussten Einsatz von (Software-)Artefakten, Bibliotheken und Paket-Managern erleichtern. Im .net-Umfeld ist Nuget der wohl populärste Paket-Manager. Der Vorteil: Einmal für regelmäßig auftretende Anforderungen erstellte und standardisierte Bibliotheken können gepackt, über ein zentrales Repository verteilt und in zukünftigen Anwendungen wiederverwendet werden. Nicht wirklich Low-Code, ermöglicht es aber eine starke Vereinfachung und Beschleunigung beim Erstellen von Anwendungsprototypen.

Eine Bibliothek mit häufig in meinen Projekten eingesetzten Basis-Klassen und -Implementierungen sind zu finden bei Nuget mit Beispielcodes gehostet auf Github.

Team und Tools

Es liegt in der Natur der Idee, dass sich in einem agilen und interaktiven Arbeitsprozess (Software-)Versionen und Spezifikationen schnell und regelmäßig ändern. Damit ist es nicht nur wichtig, dass alle Team-Mitglieder Zugang zu den für sie relevanten Informationen erhalten, sondern auch regelmäßig auf den aktuellen Stand gebracht werden. In diesem Zusammenhang ist es meist vorteilhaft eine Person zu benennen, die sich gezielt um die Vorbereitung und Strukturierung der Informationen sowie ggf. die Leitung von Sitzungen und Besprechungen kümmert.

Für externe Anwendungen, Bibliothekten und Tools sollte sichergestellt sein, dass alle Team-Mitglieder – soweit notwendig – ausreichende Kenntnisse besitzen. Falls dies (z.B. aufgrund von Kosten, Zeitplänen oder Qualifikation) nicht möglich ist, macht es häufig auch Sinn externe Spezialisten hinzuzuiehen.

Zeit- und Projektmanagement

Aus technologischer Sicht, erhält man mithilfe des Rapid Prototyping-Ansatzes schnell eine erste Testversion. Man sollte jedoch nicht nur darauf achten sich vorab die notwendige Zeit zu nehmen um konkrete und klare Anforderungen zu entwickeln, sondern:

  • Den Testnutzern – sofern vorhanden – ausreichend Zeit einräumen sich auch mit dem Prototypen auseinanderzusetzen
  • Feedback, z.B. durch Interviews, einholt und strukturiert
  • Regelmäßig eine (Neu-)Bewertung durchführt.

Ein Artikel aus dem Jahr 2018 stellt zudem eine schöne Liste typischer Fallstricke im Rahmen eines Prototyp-basierten Projekts dar.

Wie war das gleich nochmal?
Passwortschutz aus Word-Dokumenten entfernen

Die Word-Datei ist passwortgeschützt, aber niemand erinnert sich an das Passwrot? Kein Problem!

Ich bin mir ziemlich sicher, dass jeder von uns bereits schon einmal in diesem oder einem ähnlichen Dilemma war: Sie versuchen auf ein geschütztes Dokument oder einen passwortverschlüsselten Datensatz zuzugreifen, den Sie seit Jahren nicht mehr benötigt haben – und Sie haben das Kennwort vergessen. So aktuell mir geschehen mit einem vor mehr als vier Jahren für einen Kunden erstellen und gegen (versehentliche) Bearbeitung geschützten Word-Formular, das nun doch bearbeitet werden soll.

Trotz intensivster Suche im Passwortmanager und/ oder diversen Readme-Dateien lässt sich natürlich genau dieses Kennwort nicht finden. Was tun?

Eine Schnellsuche beim Internetsuchdienst meines Vertrauens rät:

  • Mittels STRG+A den kompletten Inhalt aus dem Dokument markieren, kopieren und in ein neues Dokument einfügen – funktioniert zumindest in meiner Word-Version (Office365, Januar 2020) nicht
  • Die Datei als .odt oder .rtf neu abspeichern und anschließend öffnen – erzeugt zwar eine bearbeitbare Kopie, entfernt aber auch die Formularfeldfunktionen; also nur eine Notlösung

Ein Artikel aus dem Jahr 2012 hat mich dann aber doch auf die richtige Lösung gebracht:

Im Grunde ist eine .docx-Datei ein ZIP-Container, der u.a. verschiedene XML-Dateien mit dem Dokumententext und den -Eigenschaften beinhaltet. Ändert man die Dateiendung in .zip, lassen sich die einzelnen Parts einer .docx-Datei mithilfe eines üblichen Archivmanager durchsuchen und bearbeiten.

Zwar hat sich inzwischen das Dateiformat gegenüber dem Artikel aus dem Jahr 2012 verändert, innerhalb des word-Verzeichnisses gibt es aber eine Datei namens settings.xml, in der ein XML-Knoten namens w:documentProtection zu finden ist.

Löscht man nun diesen gesamten Knoten, speichert die settings.xml und ändert anschließend die Dateierweiterung des Dokuments wieder in .docx um, erhält man eine ungeschützte und passwortfreie Datei, die sich nicht nur problemlos in Word öffnen lässt, sondern auch noch alle (Formular-)Features aufweist.