Gesucht, gefunden:
Ein wiederverwendbarer Extensions-Manager für .net

Das Managed Extensibility Framework – richtig eingesetzt, eine leichtgewichtige Alternative zu Bibliotheken wie Prism & Co.

Die Zeiten, in denen neue Anwendungen in Form monolither Softwaremonster entwickelt worden sind, sind (hoffentlich) inzwischen überall vorbei. Während gerade bei komplexen und/ oder über viele Dienste verteilten Anwendungen häufig eine Microservice-Architektur zum Einsatz kommt, profitieren häufig aber auch zunächst nur für den lokalen Einsatz konzipierte Anwendungen von einem gewissen Grad der Modularisierung:

  • Neue Funktionen können auch ohne ein komplettes Refactoring hinzugefügt werden; häufig auch solche, die im Rahmen eines ersten Konzepts noch gar nicht vorgesehen waren
  • Der Wartungsaufwand bzw. die Fehlersuche kann deutlich reduziert werden indem die Gesamtanwendung in kleine, gut wartbare Codeabschnitte zerlegt wird
  • Mehrere Programmierer oder Teams können parallel an der Entwicklung der Software arbeiten, z.B. indem ein Team sich auf den Anwendungskern und die Schnittstellen konzentriert, während sich andere Entwickler mit der Implementierung der (Fach-)Funktionen befassen

Kernstück dieses Design-Konzepts ist die lose Kopplung der Plugins, wobei eine Hostanwendung die notwendige Schnittstellen und Basis-Datentypen bereitstellt und sich anschließend um das Auffinden und Instanzieren der verschiedenen Schnitstellenimplementierungen kümmert. Sie sorgt ebenfalls dafür, dass die verschiedenen Erweiterungen (engl. Extensions) bei Bedarf auch gut miteinander kommunizieren können.

Häufig kommen zum Aufbau einer solchen Architektur spezielle Bibliotheken von Drittanbietern zum Einsatz. Eine davon ist Prism. Nicht selten ist es dabei notwendig ganz bestimmte Design-Muster oder Namenskonventionen im Quellcode einzuhalten. Dies gilt zwar auch für das Managed Extensibility Framework (MEF), dafür ist dieses aber ohne zusätzliche Installationen, Lizenzen oder Abhängigeiten nutzbare, da es – als Bestandteil des .net-Frameworks (auch .net Core) – standardmäßig mitinstalliert wird.

Ein komplettes MEF-Tutorial würde wohl den Rahmen dieses Beitrags sprengen. Zudem gibt es bereits gute Artikel zu diesem Thema, so viel aber trotzdem dazu: MEF basiert auf Reflection, was bedeutet, dass, basierend auf einer Liste von Assemblies oder Datei- und Verzeichnispfaden, ein Katalog von Schnittstellenimplementierungen erstellt wird, der als Auflistung von Lazy-Objekten zur Verfügung steht. Mithilfe von Metadaten können die Erweiterungen beschrieben werden, so dass z.B. ein bestimmtes Plugin gefunden und ausgeführt werden kann. Stefan Henneken hat in dieses Konzept in einem entsprechenden Artikel ausführlich beschrieben.

Auch wenn MEF-Features mit wenigen Zeilen Code genutzt werden können, empfiehltsich die Erstellung einer wiederverwendbaren Bibliothek, um ggf. zusätzliche Funktionen nutzen oder bereitstellen zu können. In der von mir öffentlich bereitgestellten BYTES.NET-Bibliothek findet sich eine derartige Manager-Klasse.

ExtensionsManager stellt eine Update-Funktion zur Verfügung, die, basierend auf einem Array von Dateisystempfaden, die Plugins enumeriert und als Extensions-Eigenschaft bereitstellt. Um die Klasse möglichst flexibel zu halten, wurde sie mit generischen Typen dekoriert, so dass sie z.B. wie folgt instanziiert werden kann:

Dim _manager as ExtensionsManager(of MyInterface, MyMetadata) = new ExtensionsManager(of MyInterface, MyMetadata)

Damit dies möglich ist, musste die private Variable _allExtensions, die das Ergebnis der MEF-Katalogerstellung beinhaltet, mit ImportMany ohne Angabe eines einschränkenden Datentyps dekoriert werden. Leider unterstützt MEF aktuell keine generischen Datentypen.

<ImportMany()>
Private _allExtensions As ObservableCollection(Of Lazy(Of TInterface, TMetadata)) = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

Das bedeutet aber auch, dass MEF zunächst alle gefundenen Plugins, unabhängig vom jeweiligen Datentyp/ Interface, zurückgibt. Solange die Anwendung lediglich über eine einzelne Schnittstellendefinition verfügt, ist dies unproblematisch. Schwieriger wird es bei mehreren Interfaces.

Um dennoch auch mit multi-Interface-Anwendungen kompatibel zu sein, wurde Update mit dem Aufruf der ValidateExtensions-Methode erweitert:

Private Sub ValidateExtensions()
    _validExtensions = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

        For Each extension In _allExtensions
            Try
                If ImplementsInterface(extension) Then
                    _validExtensions.Add(extension)
                End If
             Catch ex As Exception
             End Try
         Next

End Sub

Diese Methode durchläuft alle gefunden Extensions und prüft ob das als (generischer) Datentyp definierte Interface implementiert wird. Ist dies der Fall, wird das Plugin zur Liste valider Plugins (für diesen Manager) hinzugefügt.

Neben dem (Standard-)ExtensionsManager, enthält die BYTES.NET auch noch eine ExtendedExtensionsManager-Klasse, die es ermöglicht Plugins über Pfade zu finden, die mithilfe von Wildcards definiert wurden bzw. Erweiterung mithilfe einer ID in die Ausgabeliste hinzuzunehmen oder davon auszuschließen. Ein entsprechendes Beispiel ist in der Beispiel-Anwendung im zugehörigen Github-Repository zu finden.

Das Kinderzimmer im Code:
Polymorphismen in der Programmierung

Was sind Überschreibungen, Überladungen und Polyfills – und wozu benötigt man diese überhaupt?

Wer (mindestens) ein kleines Kind zu Hause hat, kennt sicher das folgende Phänomen: An einem Tag steht die Spielküche noch in der Ecke hinten links, aber schon am Folgetag hat der Nachwuchs das gesamte Zimmer umgeräumt, so dass nicht nur die Spielküche in eine andere Ecke bugsiert wurde, sondern nahezu jedes nur irgendwie bewegliche Möbelstück seine Position verändert hat.

Auch wenn mir wohl jeder zusatimmen wird, dass sich das Erscheinungsbild des Zimmers eindeutig verändert hat, ist es immer noch als Kinderzimmer zu erkennen. Damit unterliegt Das Zimmer einem Effekt, den man in der Programmierung Polymorphismus nennt.

Zugegeben, dieser Vergleich hinkt ein Wenig (wie wohl die meisten Vergleiche), denn eigentlich müsste ergänzt werden „während Sie das Zimmer am ersten Tag noch durch die Tür betreten haben, mussten Sie am Folgetag durch das Fenster hereinklettern“ oder „obwohl mithilfe einer Trockenbauwand das Zimmer um einige Quadratmeter verkleinert wurde, hat die Spielküche noch hineingepasst“, doch das Grundkonzept sollte klar sein: Polymorphismen treten in der objektorientierten Programmierung immer dort auf wo zur Laufzeit ähnliche Funktionen unter leicht varrierenden Bedingungen benötigt werden oder Methoden an Laufzeitbedingungen angepasst werden müssen. Dazu gehören z.B.:

  • Unterschiedliche Interpreter oder Laufzeitumgebungen, in denen der gleiche Code (z.B. ein Skript) ausgeführt werden soll – z.B. unterschiedliche Webbrowser, in denen ein-und-dieselbe HTML-Website gerendert werden soll
  • Aufruf von Methoden mit Übergabe unterschiedlicher Argumente, deren vorherige Umwandlung zu aufwändig oder schlicht nicht möglich wären
  • Methoden, die zur Laufzeit unterschiedliche Werte(-Typen) zurückgeben müssen, die evtl. auch nicht ohne Mehraufwände ineinander umgewandelt werden können
  • Vererbung von Klassen, bei denen z.B. eine Tochterklasse eine in der Elternklasse definierte Operation mit geänderten Parametern aufrufen muss

Polymorphismen können mithilfe einer Vielzahl verschiedener Techniken erreicht werden. Gerade bei der Softwareentwicklung mittels .net ist wohl die Überladung eine der am häufigsten eingesetzten Technik.

//version 1 
public int GetSum(int val1, int val2)
{ 
    return val1 + val2; 
} 

//version 2 
public int GetSum(int[] values)
{ 
    int result = 0; 
    
    foreach(int val in values)
    { 
        result += val; 
    } 
    
    return result; 
} 

//version 3 
public string GetSum(string val1, string val2)
{ 
    int result = int.Parse(val1) + int.Parse(val2); 
    return result.ToString(); 
}

Wie in diesem Beispiel, besitzen sich überladende Methoden zwar den gleichen Namen, unterscheiden sich aber in den Argumenten und/ oder Rückgabetypen.

Das Überschreiben ist eine Methode, die gern im Zusammenhang mit Klassenvererbung eingesetzt wird. Dabei haben die Methoden in der Eltern- und auch der Kinderklasse den gleichen Namen, jedoch wird die Methode in der abgeleiteten Klasse aufgerufen – anstatt die in der Elternklasse. Voraussetzung dafür ist es (in C#), die Methode in der Elternklasse als virtual zu definieren und die Methode in der Kinderklasse mit override zu dekorieren.

//die Eltern-Klasse 
public class ParentClass
{ 
    public virtual void DoSomething()
    { 
        MessageBox.Show(DateTime.Now.ToString()); 
    } 
} 

//die Kind-Klasse 
public class ChildClass : ParentClass
{ 
    public override void DoSomething()
    { 
        MessageBox.Show("Hello World!"); 
    }
}

Eine besondere Form des Polymorphismus hat sich in JavaScript herausgebildet: Häufig müssen Eigenheiten der verschiedenen Browser bei der Erstellung von Methoden berücksichtigt werden. Häufig unterstützen einzelne Browser Variablen oder Methoden, die ein anderer gar nicht kennt und umgekehrt. Ein Beispiel dafür, ist document.documentMode in Microsofts Internet Explorer:

if (document.documentMode == 7) { 
     doSomethingIn.IE7_only; 
} else { 
     doSomething.everwhereElse; 
}

Eine Prüfung auf eine dieser Eigenschaften kann in der Praxis dafür verwendet werden um einen bestimmten Browser(-Typ) zur Laufzeit zu identifizieren und spezifische Operationen auszuführen.

Mithilfe sogenannter Polyfills ist es möglich in JavaScript Methoden für einen Browser zur Verfügung zu stellen, die dieser nativ gar nicht zur Verfügung stellt – ggf. auch um Code kompatibel zu älteren Browser-Versionen zu machen. So wird die string.startsWith()-Methode erst ab Version 12 nativ durch den Internet Explorer unterstützt, mithilfe eines Polyfills ist es aber möglich ältere Systeme damit „nachzurüsten“. Damit agieren Polyfills ähnlich den Extension-Methoden in z.B. VB.net oder C#.

//add the 'string.startsWith()' function (polyfill) (e.g. for the Internet Explorer) 
if (!String.prototype.startsWith) { 
    String.prototype.startsWith = function(searchString, position) { 
        position = position || 0; 
        return this.indexOf(searchString, position) === position; 
    }; 
}

Neben der Vererbung von Klassen selbst, gehören Polymorphismen zu den im Rahmen von Clean Code-Initiativen am stärksten diskutierten und kritisierten Themen. Als Hauptargumente gegen derartige Techniken wird häufig eine erhöhe Komplexität bzw. Unübersichtlichkeit des Quellcodes aufgeführt. Ich gebe diesen Kritikern recht, dass zumindest Überladungen und Überschreibungen zu Verwirrung und Code-Wiederholungen führen können, jedoch bin ich mir auch nicht ganz sicher wie wartbar ein Quellcode wird, in dem ich z.B. ständig vermeidbare Typen-Konversionen durchführen muss oder während der Laufzeit nicht in der Lage bin flexibel auf Abweichungen reagieren zu können.

Sofern notwendig, würde ich lieber für einige Tage durch das Fenster in das Kinderzimmer klettern, als die Wand zu durchbrechen um eine zweite Tür einzubauen, die u.U. die Statik des Gesamtgebäudes gefährdet.

Schlangenbeschwörer:
Externe Skripte in .net-Anwendungen

Wie lassen sich externe Skripte in eine .net-Anwendung integrieren?

Nicht immer ist es möglich oder sinnvoll Anwendungen „aus einem Guss“ bzw. in einer Programmiersprache zu entwickeln oder zu betreiben. Unabhängig vom Konzept der Modularisierung, sollte, meiner Meinung nach, auch immer der Ansatz der „Best Available Technology (BAT)“ (im Deutschen wohl am besten zu übersetzen mit „Stand der Technik“) berücksichtigt werden. Viel zu häufig wird – gerade bei Konzeptionierung neuer Anwendungen – die Wahl der verwendeten Sprachen, Frameworks und Techniken darauf basierend getroffen, welche Kenntnisse der jeweilige Dienstleister anbieten kann und nicht welche Auswahl für die Lösung einer bestimmten Fragestellung am sinnvollsten wäre. Auch kann die Kombination relativ junger Technologien mit bereits etablierten Frameworks eine Herausforderung darstellen. Dazu ein konkretes Beispiel:

Eine bereits existierende, auf .net basierende Anwendung soll um Features aus dem Bereich Deep Learning erweitert werden. Neben den .net-Entwicklern, arbeiten an diesem Projekt natürlich auch Data Scientists mit, die u.a. für die Entwicklung der künstlichen neuronalen Netzwerke zuständig sind – dies aber natürlich in Python tun. Die Aufgabe besteht nun darin diese beiden Technologien zu kombinieren. Was also tun?

Option 1: Warum (überhaupt) Python?

Mit ML.Net steht ein Framework aus dem Hause Microsoft zur Verfügung, mit dessen Hilfe sich verhältnismäßig einfach machine learning-Lösungen in C# erstellen lassen. Die Daten-Wissenschaftler würden also (versuchen) ihre Konzepte in einer Sprache zu realisieren, die eine Integration unnötig macht und ggf. im Rahmen eines kompilierten Assemblies recht einfach in die bestehende Anwendung integriert werden kann.

Unabhängig davon ob sich die betroffene Fachgruppe darüber freuen würde bei der Wahl der zu verwendenden Tools eingeschränkt zu werden, gibt es einige Gründe warum sich Python und damit verbundene Frameworks im Bereich machine learning durchgesetzt haben. Mit dieser Lösung würde man sich ggf. einen Sonderfall ins Haus holen, bei dem man nur mit deutlich erhöhtem Aufwand auf dem aktuellen Stand der Technik bleiben und somit gegen den BAT-Grundsatz verstoßen.

Option 2: Python in .net interpretieren

Es gibt eine Reihe recht guter Projekte, wie z.B. Python.net oder IronPython, die Bibliotheken bereitstellen um Python-Skripe in .net-Anwendungen auszuführen und zu interpretieren. Mithilfe verschiedener Injection-Szenarien ist es sogar möglich Variablen und Funktionen bereitzustellen, die üblicherweise nicht Bestandteil der (Core-)Python-Umgebung sind.

Leider kommen diese Interpreter gerade bei wissenschaftlichen Bibliotheken an ihre Grenzen. Um z.B. aufwändige mathematische Operationen oder (wissenschaftliche) Berechnungen durchführen zu können, stellen Frameworks wie Anaconda zusätzliche Funktionen und Bibliotheken bereit, die bei den genannten Interpretern nicht zur Verfügung stehen. Somit stellt dies nur eine engschränkt nutzbare Lösung dar.

Option 3: Die Python-Skripte als externe Prozesse ausführen

In einem Artikel in der dotnetpro vom März 2019 wird – neben einem Beispiel für die Verwendung von IronPython – genau dieses Szenario beschrieben, mit dem Lösungsansatz das Python-Skript mithilfe der .net-eigenen Process-Klasse aufzurufen, die Ausgabe zu überwachen und in der Core-Anwendung abzuwarten bis das Skript vollständig durchlaufen wurde bis die entstandenen Daten weiterverarbeitet werden.

Natürlich funktioniert dies nicht nur für Python-Skripte, sondern für alle Arten externer Skripte und Anwendungen – und das ganz ohne z.B. tiefgreifende Anpassungen an der Original-Anwendung durchführen zu müssen. Möchte man jedoch nicht jedes mal aufs Neue

  • die Process-Parameter, inkl. ggf. notwendiger für das externe Skript oder die Anwendung notwendige Kommandozeilenparameter, konfigurieren
  • den Ablauf überwachen und auf auftretende Output- oder Fehler-Events reagieren

empfiehlt sich die Kapselung in eine dafür optimierte Klasse.

Mit der ManagedProcess-Klasse stellt das aktuelle Release (1.4.1) des BYTES.NET-Frameworks genau eine derartige Kapselung bereit.

Private Async Sub RunScript()

    'create the process class instance
    Dim process As ManagedProcess = New ManagedProcess

    With process
        .Executable = "C:\Windows\System32\cmd.exe"
        .Arguments = {"/c " & """" & "%InstallationDir%..\..\..\..\samples\Scripts\LongRunningSampleScript.cmd" & """" & " TestValue"}
        .ShowUI = Me.ShowUI
    End With

    'handle the events
    AddHandler _process.OutputReceived, Sub(data As LogEntry)
                                            _log.Add(data)
                                        End Sub

    AddHandler _process.Exited, Sub(code As Integer)
                                    MsgBox("Process finished with: " & code)
                                End Sub

    Await _process.RunAsync()

End Function

Mithilfe weniger Zeilen Code ist es z.B. möglich ein Skript (mithilfe eines beliebigen externen Interpreters) auszuführen und sowohl auf Rückgaben zu reagieren als auch auf den Abschluss des Runs zu warten. Zudem bietet die Klasse die Vorteile, dass

  • mit RunAsync bereits eine Methode bereitgestellt wird, die die Nutzeroberfläche während (eines beliebig langen) Durchlaufs nicht blockiert
  • ein einheitliches Event (OutputReceived) zur Verfügung steht, dass die Verarbeitung der Rückgaben ermöglicht; Fehler können mithilfe des InformationLevel des zurückgegebenen LogEntry-Objekts identifiziert werden
  • relative Pfade – z.B. mithilfe der generischen %InstallationDir%-Variable – für den Interpreter-Pfad unterstützt werden, so dass auch Interpreter wie z.B. Anaconda in einer portable-Version (z.B. als Bestandteil eines Installationspakets) genutzt werden können

Ein detailliertes Beispiel für den Einsatz mit Windows-Batch (*.cmd) und Python (*.py) ist im zugehörigen Repository auf Github verfügbar.

Der Geist in der Grafikkarte:
NVIDIAs GPC2020

Im Rahmen der GPC 2020 kommen Spezialisten aus allen Fachbereichen zusammen – in diesem Jahr online

Aufgrund der durch das COVID-19 hervorgerufenen, geänderten Rahmenbedingungen, findet NVIDIAs diesjährige GPU Technology Conference als digitales Online-Event statt, anstatt wie ursprünlich geplant in San Jose. Da die Anmeldung kostenfrei ist, kommt man auf diese Weise in den Genuss teils hochspezialisierter Fachvorträge, ganz ohne Ausgaben für Reise oder Hotel.

Neben einer Vielzahl von Kursen, Workshops und Postern zu den verschiedensten Anwendungsfällen und Teilaspekten von High Performance Computing (HPC) und Artifical Intelligence (AI), werden auf der GTC Tools und Lösungen präsentiert, die die Entwicklung und Integration von Deep Learning (DL)-Ansätzen weiter vereinfachen und somit noch massentauglicher machen sollen. Um dieses Ziel zu erreichen, findet man im Session-Katalog auch immer wieder Vorträge, wie der von Will Ramey, in dem Grundlagen zum Thema künstliche Intelligenz erklärt werden

Doch weshalb betreibt eine Firma, die die meisten Endanwender lediglich als Hersteller von Grafikkarten kennen, einen derartigen Aufwand?

Die Antwort ist recht einfach: Um Grafikkarten zu verkaufen.

Zugegeben, diese Darstellung ist sehr stark vereinfacht, jedoch verdiente NVIDIA, nach Zahlen vom März 2020, etwa 9,5 Milliarden US-Dollar mit dem Verkauf von Graphics Processing Units (GPUs) und lag damit im Q4 2019 deutlich vor dem Mitbewerber AMD.

Warum aber profitiert gerade ein Hersteller von Grafikkarten vom Trendthema AI – und nicht Firmen wie Intel oder AMD, deren Prozessor-Chips (CPU) in praktisch jedem Heimcomputer und Server verbaut sind?

Technisch gesehen ist auch diese Antwort recht einfach: Grundsätzlich basieren die meisten DL-Modelle auf der gewichteten, stufenweisen Verarbeitung von Parametern. Tim Dettmers hat das Grundkonzept hinter Deep Learning in einem Artikel im NVIDIA Developer Blog zusammengefasst.

Mathematisch wird dies über sogenannte Matrix-Multiplikationen abgebildet – und genau da kommt der Vorteil der GPU gegenüber der CPU zum Tragen: Grafikkarten wurden ursprünglich genau dazu entwickelt diese mathematischen Operationen möglichst performant durchzuführen. Im direkten Vergleich kann bereits eine GPU deutlich mehr Bilder pro Sekunde verarbeiten als ein Verbund aus 5 CPUs – und die Leistungsfähigkeit der GPUs hat sich in den letzten Jahren immer weiter erhöht.

Auch wenn die prominentesten Beispiele für Deep Learning oder AI aus den Bereichen der Bildverarbeitung oder autonomen Fahren kommen, sorgt die Zunahme sogenannter Edge Devices als Bestandteil des IoT dafür, dass immer größere Datenmengen zu verarbeiten, (vor) zu klassifizieren und für (de-)zentrale Erfassungs- und Verarbeitungssysteme aufzubereiten sind. Um dies zu realisieren, genügen traditionelle, regelbasierte Systeme of nicht mehr. Ein Ansatz zur Lösung dieser Herausforderungen is Deep Learning oder AI – dazu bedarf es jedoch neben der passenden Hard- und Software auch Wissenschaftler und Spezialisten die in der Lage sind mit diesen Systemen zu arbeiten.

Richtig gebunden:
Emby im Active Directory

Der Medienserver Emby lässt sich ohne großen Aufwand an ein LDAP anbinden. Doch was ist bei der Verwendung von Active Directory zu beachten?

Ich bin ein ziemlicher Fan von Emby. Einmal eingerichtet, stellt der Emby Server eine recht unkomplizierte und elegante Möglichkeit zur Verfügung seine privaten Medien – wie z.B. Musikdateien – in Bibliotheken zu organisieren und auf nahezubeliebigen Geräten wiederzugeben. Mithilfe verschiedener Plugins lassen sich zudem externe Quellen, wie z.B. IP TV, einbinden und der Funktionsumfang erweitern.

Eines dieser Plugins erlaubt es auch den Medienserver an das lokale Active Directory und damit auch das zentrale Nutzermanagement anzubinden. Folgt man der zugehörigen Anleitung, ist die Installation selbst schnell erledigt – leider ist die Konfiguration nicht ganz so intuitiv wie ich es erwartet hätte. Obwohl nur aus einer Seite mit ein paar Textfeldern bestehend, hat es mich einigen Aufwand gekostet die richtigen Konfigurationsparameter zu setzen:

Während LDAP server url und LDAP server Port number im Grunde selbsterklärend sind, muss ich zugeben, dass ich bereits für Bind DN auf einen Thread aus dem Community-Forum zurückgreifen musste. Es ist zwar nicht sonderlich überraschend, Authentifizierungsdaten für eine LDAP-Abfrage bereitstellen zu müssen und ich behaupte einfach mal, dass ich kein absoluter Newbe bei der Integration mit LDAP bin, aber den Distinguished Name komplett angeben zu müssen, kommt bei den meisten Anwendungen nicht so häfig vor. Nchdem man diesen dann jedoch ausgefüllt und noch das zugehörige Passwort in Bind credentials hinterlegt hat, kommt der Konfigurationsparameter, der mich einige Stunden an Suche und Nerven gekostet hat: User search filter.

Verwendet man ein Windows Active Directory als Backend und belässt gleichzeitig den standardmäßigen Filter (uid={0}), kann man noch so verzweifelt versuchen sich mit einem AD-Nuterkonto anzumelden, es wird immer die Fehlermeldung auftauchen „Nutzerdaten falsch; Anmeldung verweigert“. Im Emby Server Log sieht das Problem in etwa so oder so ähnlich aus:

Die Ursache: Relativ häufig ist das uid-Attribut standardmäßig nicht mit dem Nutzernamen belegt, so dass der Versuch den Nutzer bei der Anmeldung zu intentifizieren ins Leere läuft. Die Lösung: Verwendung des sAMAccountName-Attributes mit dem Filter (sAMAccountName={0}). Da dieses Attribut in einem AD als einmaliger Schlüssel verwendet wird, ist er immer mit dem Nutzernamen belegt. Eine vollständige Konfiguration würde dann wie folgt aussehen:

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.