Mit AvaloniaUI Enums in XAML zeigen

Es geht um das GUI-Framework AvaloniaUI. Und es geht darum, wie man in der darin üblichen XAML-Beschreibung Enum-Werte in z.B. eine Combobox hineinbekommt – ohne extra Code.

Es gibt gewisse Kritik an diesem Vorgehen vor allem aus dem Kreise der AvaloniaUI-Kernprogrammierer. Man möchte lieber für alles und immer View-Modelle (MVVM) verwenden. Zweifellos, das ist gut. Aber es gibt auch Gründe, es anders herum zu machen. Zum Beispiel, wenn man einfach nur schnell etwas zusammenstecken möchte. Oder gerade eben kein Viewmodell will.

Zur Lösung: Man möchte z.B. diese GUI darstellen, wobei in der Combobox die Items aus einem eigenen oder bekannten Enum stammen sollen und selektierbar sein sollen. Hier: Dock aus dem DockPanel.

In dieser kleinen Demo wird die Combobox durch den Enum Dock gefüllt und im Weiteren dieser Selektionswert an die Dock-Eigenschaft des kastanienfarbenen Rechtecks gebunden. De XAML oder aXAML-Code dazu sieht so aus:

<UserControl xmlns="https://github.com/avaloniaui"
             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:adb="https://flinkebits.de/avadevbox"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaControls.Demo.RangeSliderDemo">
  <StackPanel>
    <TextBlock>Aufzählungswerte von Dock:</TextBlock>
    <ComboBox Name="cbdock" Width="240"
              HorizontalAlignment="Left"
              SelectedIndex="2" Margin="10,5,0,30"
              Items="{Binding Source={adb:EnumBindingSource {x:Type Dock}}}"/>

    <Border BorderThickness="3" BorderBrush="AliceBlue" Margin="10" Height="300">
      <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Rectangle DockPanel.Dock="{Binding #cbdock.SelectedItem}"
                   Width="55"
                   Height="44"
                   Fill="Maroon" />
        <Button IsEnabled="False" >Rest</Button>
      </DockPanel>
    </Border>
  </StackPanel>
</UserControl>

Früher in WPF hat man dazu nette Verrenkungen mit ObjectValueProvider gemacht. Doch den gibt es unter Avalonia nicht mehr. Stattdessen greife ich hier auf eine Markupextension zurück: EnumBindingSource. Allerdings gibt es Markupextensions in AvaloniaUI auch nicht so wirklich. Auf jeden Fall gibt es keine Ableitung von MarkupExtension. Das Problem für AvaloniaUI ist, dass man eigentlich gleichzeitig von AvaloniaObject und MarkupExtension erben wöllte, aber natrülichin C# nur eine Klasse beerbt werden kann!

Die Lösung ist, dass die XAML-Komponente von AvaloniaUI die Klasse MarkupExtension vollständig ignoriert und bei Verwendungen wie Markupextensions einfach nach Klassen sucht, die lediglich von AvaloniaObject abgeleitet sind und eine von mehreren möglichen public ProvideValue()-Signaturen hat. Avalonia lässt hier verschiedene Rückgabewerte zu. Konkrete und object, sowie verschiedene Parameter. Somit lässt sich diese Extension so schreiben:

public class EnumBindingSource : AvaloniaObject /*: MarkupExtension*/
{
    private Type _enumType;
    public Type EnumType
    {
        get { return this._enumType; }
        set
        {
            if (value != this._enumType)
            {
                if (null != value)
                {
                    Type enumType = Nullable.GetUnderlyingType(value) ?? value;

                    if (!enumType.IsEnum)
                        throw new ArgumentException("Type must be for an Enum.");
                }

                this._enumType = value;
            }
        }
    }

    public EnumBindingSource() { }

    public EnumBindingSource(Type enumType)
    {
        this.EnumType = enumType;
    }

    public Array ProvideValue(IServiceProvider serviceProvider)
    {
        if (null == this._enumType)
            throw new InvalidOperationException("The EnumType must be specified.");

        Type actualEnumType = Nullable.GetUnderlyingType(this._enumType) ?? this._enumType;
        Array enumValues = Enum.GetValues(actualEnumType);

        if (actualEnumType == this._enumType)
            return enumValues;

        Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
        enumValues.CopyTo(tempArray, 1);
        return tempArray;
    }
}

Das Ganze geht noch besser, denn man könnte auch noch die Attribute DescriptionAttribute auf den Aufzählungswerten auswerten und damit eine Lokalisierung anbieten. Das geht natürlich genau so, dass der SelectedValue vom Enum-Typ ist und die Anzeige in der Combobox der Description-Text ist.

Aber das überlasse ich einer Übung des Lesers. Es gibt genug WPF-Beispiele, die genau das tun.

Refaktorisieren

Heute wollen wir uns mal mit dem Thema Refaktorisieren/Refactoring beschäftigen. Wir schauen uns mal an, was es ist, und wie und ob es bei Softwareprojekten sinnvoll einzusetzen ist. Kurze Antwort: Refaktorisierungen sind ein extrem nützliches Werkzeug zur Verbesserung der Codequalität.

Definition

Die Wikipedia schreibt dazu: Refaktorisierung (auch Refactoring, Refaktorierung oder Restrukturierung) bezeichnet in der Software-Entwicklung die manuelle oder automatisierte Strukturverbesserung von Quelltexten unter Beibehaltung des beobachtbaren Programmverhaltens. Dabei sollen Lesbarkeit, Verständlichkeit, Wartbarkeit und Erweiterbarkeit verbessert werden, mit dem Ziel, den jeweiligen Aufwand für Fehleranalyse und funktionale Erweiterungen deutlich zu senken.

Martin Fowler, Autor des Buchs Refactoring, hat das Thema populär gemacht. Sehen wir uns hier seine Definition an: Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations, each of which „too small to be worth doing“. However the cumulative effect of each of these transformations is quite significant. By doing them in small steps you reduce the risk of introducing errors. You also avoid having the system broken while you are carrying out the restructuring – which allows you to gradually refactor a system over an extended period of time.

Hintergrund

Hinter dem Refaktorisieren steht eine mathematische Theorie. Es handelt sich um Gruppenopertionen und man kann die Beweise führen, dass gewisse Refaktorisierungsoperationen den Programmkode bedeutungsidentisch transformieren. So, als ob funktionell nichts geschehen sei. Per Induktion kann man dann daraus ableiten, dass eine Folge von Refaktorisierungsoperationen auch Bedeutungserhaltend sind. Somit ist also der Gesamte Vorgang nicht Bedeutungsändernd.

Praxis

In der Praxis jedoch wird man aber genau deshalb Refaktorisieren, weil man eine Änderung oder Erweiterung mit den bisherigen Strukturen nicht mehr leisten kann. Man benötigt also eine Abstraktion und sobald man diese eingezogen hat, wird man wechselweise Kodeänderungen und Refaktorisierungen durchführen. Somit ist die Gesamtoperation natürlich nicht Bedeutungserhaltend. Allerdings war ja auch das glatte Gegenteil das Ziel: Ein Entwickler sollte ja die Kodebasis irgendwie erweitern. Und damit das Verhalten Ändern.
Das Gute an der Sache ist eben, dass das Refaktorisieren zunächst ein Schritt ist, der in der Theorie keine Bedeutungsänderung bringt.

Beispiele

An dieser Stelle sollten wir den Blick mal auf einige Beispiele von Refaktorisierungsoperationen werfen, um ein wenig mehr Gefühl dafür zu bekommen.

Hier einige typische Refaktorisierungen:

Umbenennen

Das einfachste ist sicherlich das Umbenennen. Natürlich stellt sich die Frage was. Man kann ja eine lokale Variable, eine Elementvariable, Klasse, Templateparameter, Elementfunktion, Namespace u.v.a.m umbenennen. Die Implikationen sind jeweils ähnlich; können aber weite Kreise ziehen. Inklusive eventueller Dateiumbenennungen. So lange keine Konflikte entstehen, sollte evident sein, dass dies eine harmlose Operation ist:

private static void Gelder(int zahl)
{
    int doppelt = zahl + zahl;
    Console.WriteLine($"{zahl} Heller verdoppelt sind {doppelt} Heller");
}

public static void Main(string[] args)
{
    Gelder(zahl: 4);
}

Durch Umbenennung des Parameters zahl in nummer werden alle Stellen kohärent angepasst.

private static void Gelder(int nummer)
{
    int doppelt = nummer + nummer;
    Console.WriteLine($"{nummer} Heller verdoppelt sind {doppelt} Heller");
}

public static void Main(string[] args)
{
    Gelder(nummer: 4);
}

Extrahieren und Entrollen einer Variable

Hierbei handelt es sich um zwei gegenteilige Operationen. Extrahieren bedeutet, dass aus einem Ausdruck eine neue Variable entsteht. Entrollen ist das Gegenteil. Alle Vorkommen einer Variable werden durch den Ausdruck ersetzt. Sehen wir folgendes Beispiel an:

private static (double umfang, double fläche) Kreis(double radius)
{
    double rp = radius * Math.PI;
    double umfang = 2 * rp;
    double fläche = radius * rp;
    return (umfang, fläche);
}

Der Teilausdruck radius * Math.PI wurde hier ein eine Variable rp gepackt. Das könnte vielleicht eine Rechnung ersparen, verringert aber eher die Lesbarkeit. Daher konnte die Variable rp entrollt werden und an ihrer Stelle steht nun der Ausdruck. Dieser Umbau kann durch Extraktion umgekehrt werden.

private static (double umfang, double fläche) Kreis(double radius)
{
    double umfang = 2 * radius * Math.PI;
    double fläche = radius * radius * Math.PI;
    return (umfang, fläche);
}

Skalar zu Tupel-Transformation

Diese Refaktorisierungsoperation habe ich mir wohl selbst ausgedacht, da ich sie so noch nie gesehen habe. Es handelt sich um das umkehrbare aufblasen einer skalaren Variable in eine Kompositvariable. Also typischerweise in eine Liste, Feld, Tupel oder Struktur/Objekt. Der Anwendungsfall ist in typischen Applikationen recht häufig. Werfen wir einen Blick auf diese sehr vereinfachte Businessanwendung, die eine Person mit ihrer Adresse ausgibt:

private class Anschrift
{
    public string Straße;
    public int Nr;
}
private struct Person
{
    public string Name;
    public Anschrift Adresse;
}
private static void WriteAdresse(Person p)
{
    Console.WriteLine("Name :" + p.Name);
    Console.WriteLine($"Wohnhaft in {p.Adresse.Straße} {p.Adresse.Nr}");
}

Nun stellt sich heraus, dass eine Person mehrere Adressen haben kann. Also aus dem Skalar Adresse wird eine Liste von Adressen. Die Funktion bleibt zunächst dieselbe. Allerdings wird der Zugriff auf die Adresse nun mit einem Indexzugriff verziert. Die Funktion bleibt dieselbe:

private class Anschrift
{
    public string Straße;
    public int Nr;
}
private struct Person
{
    public string Name;
    public List<Anschrift> Adresse;
}
private static void WriteAdresse(Person p)
{
    Console.WriteLine("Name :" + p.Name);
    Console.WriteLine($"Wohnhaft in {p.Adresse[0].Straße} {p.Adresse[0].Nr}");
}

In einem nächsten Schritt kann man in WriteAdresse eine for-Schleife einbauen, die alle Adressen ausgibt.

Klasse extrahieren

Bei dieser Operation werden Methoden mit all ihren Abhängigkeiten aus einer Klasse in eine neue Klasse extrahiert. Diese Refaktorisierung hilft insbesonder beim S in SOLID, der „Single Responsibility„. In diesem Beispiel sehen wir, wie die beiden Methoden völlig unabhängig voneinander in derselben Klasse stehen. Über die sinnhaftigkeit ist hinwegzusehen:

public class RechenKnecht
{
    private readonly int _num;
    private readonly string _name;

    public RechenKnecht(int num, string name)
    {
        _num = num;
        _name = name;
    }

    public int Rechne()
    {
        return _num * 2;
    }

    public string Gruß()
    {
        return "Edler von " + _name;
    }
}

Hier wurde die Methode Gruß() in die Klasse Ansprache extrahiert. Man hätte es natürlich auch umgekehrt machen können:

public class RechenKnecht
{
    private readonly int _num;

    public RechenKnecht(int num)
    {
        _num = num;
    }

    public int Rechne()
    {
        return(_num * 2);
    }
}
public class Ritter
{
    private readonly string _name;

    public Ritter(string name)
    {
        _name = name;
    }

    public string Gruß()
    {
        return "Edler von " + _name;
    }
}

Auch diese Opertion ist umkehrbar…. bis am Ende alles Methoden und der Gesamtzustand der Anwendung in einer Klasse ist. Das ist aber natürlich unwartbar.

Das war natürlich ein triviales Beispiel. Wäre aber der Gruß nun zusätzlich mit der berechneten Zahl ausgestattet gewesen, dann geht es so nicht. Aber man kann dann mit Komposition arbeiten. Stellen wir uns also diese Gruß()-Methode vor:

    public string Gruß()
    {
        int x = Rechne();
        return $"{x}ter Edler von {_name}";
    }

In diesem Fall kann man mit folgender Refaktorisierung arbeiten. Hierbei wurde RechenKnecht auf seine Essenz redzuiert und dem Ritter als Vehikel gegeben (Komposition). Es sind zwei Klassen entstanden:

public class RechenKnecht
{
    private readonly int _num;

    public RechenKnecht(int num)
    {
        _num = num;
    }

    public int Rechne()
    {
        return(_num * 2);
    }
}
public class Ritter
{
    private readonly string _name;
    private readonly RechenKnecht _knecht;

    public Ritter(string name, int num)
    {
        _name = name;
        _knecht = new RechenKnecht(num);

    }

    public string Gruß()
    {
        int x = _knecht.Rechne();
        return $"{x}ter Edler von {_name}";
    }
}

Elemente rauf- und runterziehen

Eine Refaktorisierungsoperation möchte ich noch zeigen, ehe ich zum Aha übergehe, eine objektorientierte: Elementfunktionen in der Erbhierarchie raufschieben oder runterziehen. Auch hier besteht wieder (muss ja) eine Umkehrbarkeit und Bedeutungsgleiche in beiden Richtungen. Sehen wir uns dieses Beispiel an. Dort wird eine Elementfunktion aus der Vaterklasse aufgerufen. Außerdem ist die Methode in der Schnittstelle deklariert. Da aber niemand sonst darauf zugreift, kann der Kode kürzer und enger gefasst werden:

public interface IRechner
{
    int Quadrat(int num);
    void Rechne(int num);
}

public class BasisRechner : IRechner
{
    public virtual int Quadrat(int num)
    {
        return num + num;
    }

    public virtual void Rechne(int num)
    {
        Console.WriteLine(num);
    }
}

public class QuadratRechner : BasisRechner
{
    public override void Rechne(int num)
    {
        Console.WriteLine(Quadrat(num));
    }
}

Da die Methode Quadrat() in BasisRechner überhaupt nicht verwendet wird, kann man sie runterschieben in QuadratRechner. Man kann sie sogar aus der Schnittstelle entfernen (soweit nie benutzt) und dabei die Funktionalität beibehalten:

public interface IRechner
{
    void Rechne(int num);
}

public class BasisRechner : IRechner
{
    public virtual void Rechne(int num)
    {
        Console.WriteLine(num);
    }
}

public class QuadratRechner : BasisRechner
{
    public override void Rechne(int num)
    {
        Console.WriteLine(Quadrat(num));
    }

    public virtual int Quadrat(int num)
    {
        return num + num;
    }
}

Es gibt natürlich noch viele weitere Refaktorisierungsoperationen. Dies sollte nur eine kleine Auswahl sein, um einen Eindruck zu vermitteln.

Anwendung

Das Ganze lässt sich natürlich wunderbar automatisieren und in Werkzeuge gießen. So sind mit z.B. ReSharper, Eclipse, Visual Studio oder IntelliJ wunderbare Refaktorisierungswerkzeuge entstanden, die das Leben eines Entwicklers wirklich erleichtern. Vor allem aber arbeiten sie als Automaten viel konistenter und korrekter als es ein Entwickler langfristig könnte….

Insofern rate ich auch grundsätzlich dazu, immer Werkzeuge für solcherlei Vorhaben zu benutzen und sich dabei unbedingt vom Rechner unterstützen lassen. Ausnahmen bestätigen die Regel…

Mit diesem Wissen und diesen Werkzeugen im Gepäck kann man natürlich auch die Qualität des Quelltexts verbessern. Ich unterscheide hier mal alltägliche und gezielte Anwendung.

Alltägliche Anwendung

In der alltäglichen Anwendung nutzt ein Entwickler die Werkzeuge während des Kodeschreibens. Erstens, um architektonische Anpassungen zu machen, um die neue Funktion einzubauen. Und zweitens, um schlicht schneller zu sein. Stellt er fest, dass ein Dreizeiler öfter mehrfach im Kode steht, kann er ‚mal eben‘ eine Funktion extrahieren. Das Werkzeug macht den Rest. Im Anschluss kann er sie noch schnell umbenennen. Oder: Im Gefecht der Entstehung neuer Kodezeilen hat der Entwickler zunächst einige temporäre Variablen angelegt. Im Weiteren Verlauf stellt er fest, dass eine davon nur an einer Stelle verwendet wird. Ergo kann er sie automatisch Ausrollen lassen und direkt durch die rechte Seite der Variable austauschen. Ein weiteres gutes Beispiel ist ein fehlender/überflüssiger Parameter an einer Methode. Werkzeug anstarten und alle Aufrufe werden angepasst. Z.B. mit einem definierbaren Standardwert.

Refaktorisieren und die Werkzeuge stellen einfach einen Produktivitätsfaktor im Entwickleralltag dar und sind heute nicht mehr wegzudenken.

Gezielte Anwendung

Unter gezielter Anwendung verstehe ich dedizierte Sitzungen, in denen Refaktorisert wird. Entweder alleine oder mehrere Leute. Also z.B. ein Codereview, Paarprogrammierung oder doch wieder der einzelne Entwickler. Es geht dabei darum, dass ein Kodeabschnitt identifiziert wurde, der architektonisch oder in seiner Art so nicht mehr tragbar ist. Man macht sich also alleine oder als Gruppe Gedanken darüber, wie man umbauen sollte und tut dies dann auch. In aller Regel kommt etwas Besseres dabei heraus. Dabei sollte man sich nicht gleich ins Bochshorn jagen lassen, wenn sich zunächst ein Tal der Tränen auftut. Mit etwas gezieltem Handeln und unbeirrbarkeit bekommt man das regelmäßig wieder hin und freut sich nacher über ein besseres Ergebnis.

Probleme und Gefahren

Die Realität ist natürlich wieder viel komplexer und gemeiner als die graue Theorie und mein Geschwafel. Gerade in der imperativen objektorientierten Programmierung (OOP) haben Objekte regelmäßig Zustand und damit werden Instanzen, Referenzen und Vielfachheiten sowie die Reihenfolge interessant. Tatsächlich funktioniert die Theorie hier auch in der Praxis, aber man refaktorisiert ja gerade an Programmstücken, die es besonders nötig haben.

Die Gottklasse entmachten

Eine häufige Ursache für Wust ist, wenn eine Klasse mehrere Aufgaben erledigt = mehrere Klassen in sich vereint. Wenn das extrem wird, nennt man das Gottklasse. Quasi immer ist das die Ursache für nicht auflösbare zirkuläre Referenzen. Daher sollte man genau danach ausschau halten und die Operation Klasse Extrahieren verwenden. Lieber einmal mehr auf Verdacht anwenden. Wenn sich eine Aufteilung von Methoden und Elementvariablen finden lässt, dann war es der Fall und es sollte dann in mehrere Klassen Aufgespalten werden.

Dies ist eine der erfolgversprechendsten Refaktorisierungen. Im Nachgang ist es häufig so, dass eine der beiden extrahierten Klassen eine Vielfachheit bekommt (Skalar zu Tupel-Transformation).

Asynchron und Multithreading

Hier wird es spannend. Grundsätzlich sind auch hier alle Refaktorisierungen sinnvoll verwendbar und machen wenig Probleme. Die Probleme liegen allerdings im Detail. So kann es passieren, dass sich durch die neue Architektur die Lebenszyklen von Objekten verändern und dann bei Async-Programmierung Zugriffsfehler passieren. Oder Zustände sich früher/später als vor der Refaktorisierung verändern. Wie gesagt alles kann und es kommt immer auf die tatsächliche Implementierung an. Allgemein lässt sich aber sagen, dass man mit einer Architektur gut Async+Multithreading besteht, wenn sie „Single Responsibility“, „Interface Segregation“ und „Immutable Pattern“ beherzigt. Doch in diese Richtung kann und sollte man „hinrefaktorisieren“.

Resumé

Damit schließe ich diesen Artikel mal ab und resumiere, dass Refaktorisierung eine sehr nützliche Sache ist und sich fast immer lohnt. Insofern sollte ein jeder Projektleiter in der Software regelmäßig anberaumte Refaktorisierungs-Sitzungen veranstalten, in denen der Code an problematischen Stellen verbessert wird. Nebenbei helfen solche „öffentlichen Reviews“ auch frischen Etnwicklern etwas Neues zu lernen.

Automatisierte Builds in Azure DevOps/VSTS

Ein essentieller Teil einer erfolgreichen Softwareenwicklung in einem Unternehmen ist Continuous Integration, ein ständig verfügbarer kontinuierlicher Build. Und daneben gehört natürlich Continuus Deployment hinein. So werden fertige Builds auch gleich auf Herz und Nieren geprüft.

In diesem Artikel geht es darum, wie man aus einem .Net 4.7-Projekt mit mehreren Setups und exe-Dateien (GUI und CUI) einen continuus Build hinbekommt. Dabei sollen die Versionsnummern immer stetig hochgezählt werden und sowohl in jeder Assembly als auch in jedem Setup landen. Zusätzlich gibt es einige GUI-Komponenten, die das unsägliche .Net-licensing verwenden. Auch dafür gibt es eine Lösung.

Der Build wird eingerichtet auf Azure DevOps. Modernere Versionen sollten Ähnlich oder einfacher einzurichten sein.

Ziel

Folgende Probleme sollen adressiert und gelöst werden:

  • .Net Licensing von GUI-Komponenten (z.b. PerpetuumSoft)
  • Versionsnummern automatisch vergeben
  • Basis der Versionsnummer konfigurierbar (beim Build)
  • Versionsnummer in jeder Assembly (Asm. Ver + File Ver.)
  • Versionsnummer in jedem MSI, „Drüberinstallieren“ soll gehen.

Lizensierung

Mit .Net Licensing hat Microsoft ehemals ein Standardverfahren etabliert, mit dem Komponenten (v.a. Windows Forms) lizensiert werden können. Aufgrund des Alters und seiner Art, ein eher unsägliches Ding. Aber man kommt damit klar.

Die primäre Idee von .Net Licensing ist es, für GUI-Komponenten Entwicklerlizenzen während der Entwicklung durchzusetzen. Das sieht dann etwa so aus: Der Entwickler bindet eine GUI-Komponente ein und bei jeder Verwendung prüft diese, ob auf dem Entwicklerrechner eine Lizenz vorhanden ist. Ist das nicht der Fall, kann etwa ein Nag-Screen aufpoppen. Dazu gehört noch der Lizenzcompiler LC.Exe, der aus *.licx-Dateien *.licenses-Dateien baut. Das erledigt jeweils die Komponente, die auf ihre Weise checkt, ob sie auf diesem Entwicklersystem lizenziert ist. Deshalb steht auch in der *.licx-Datei eine Liste von Komponenten, die der LC.EXE aufrufen soll (per Reflection). Im Ergebnis entsteht eine *.licenses-Datei, die als Eingebettete Ressource in die Assembly eincompiliert wird. Danach ist die Komponente dauerhaft auch auf Kundensystemen lizensiert.

Das Problem ist nun: So lange alles nur auf Entwicklermaschinen lief, ging alles gut. Der CI-Build läuft aber auf einer frisch instantiierten VM in der Cloud und diese hat keine Entwicklerlizenz installiert. Ergo kann es passieren, dass ein Nag-Screen aufgeht und keiner ihn kann klicken. Der Build wird nie fertig.

Info dazu auf Stackoverflow

Lösung: Man macht einmal einen ordentlichen Build auf einem Entwicklersystem und sammelt dort die *.licenses-Dateien ein (Bin-VZ). Dann setzt man den Dateityp in der SLN von *.licx auf „None„. Sodann checkt man die *.licenses Dateien pro Assembly ein und bindet sie als „Embedded Resource“ zusätzlich in die SLN ein.

Ab da läuft der Build ohne Nag-Screen durch. Nur beim Aktualisieren der Komponenten muss man diesen Vorgang wiederholen. Man macht somit LC.EXE arbeitslos und verhindert das Aufrufen des Lizensierungs-Codes.

Versionierung

Kommen wir zum Thema Versionsnummern. Die wichtigste Zutat für einen automatiserten Build sind korrekte und konfigurierbare Versionsnummern. Nur so kann man feststellen, ob ein Artefakt neuer oder älter ist und zu welchem Versionsstand er gehört. .Net bringt uns 3 verschiedene Versionsnummern, die je Assembly gesetzt werden können. Eine Erklärung dazu und zu „Semantischer Versionierung“ findet man hier:

Umsetzung

Um eine automatische Versionierung hinzubekommen braucht man zwei Zutaten:

  • Eine Pipeline-Variable für die Assemblyversion
  • Ein Skript, um die Versionsvariable zu verrechnen
  • Ein Build-Target für MSBUILD
Die Variable BuildVersionOfAssembly wird angelegt mit dem aktuellen Standardwert

Der Plan ist es, die Versionierung so zu machen, wie es in semantic versioning empfohlen wird. Also die verschiedenen Typen von Assembly-Versionsattributen korrekt zu setzen. Sprich: AssemblyVersion und AssemblyInformationalVersion ist die ‚fixe‘ Version. Darüber referenzieren und finden sich die abhängigen Assemblies. Die letzte Stelle ist leer. Derweil ist AssembyFileVersion quasi dieselbe Nummer aber mit gesetzter Build-Nummer, die irgendwie ausgerechnet wird.

Konkret kommt über die Build-Pipeline eine Variable AssemblyVersion hinein in Form einer 3-Stelligen Version. Z.B. „11.22.33“

Dies resultiert in:
AssemblyVersion = „11.22.33“
AssemblyInformationalVersion = „11.22.33“
AssembyFileVersion = „11.22.33.1928“
Das Versions-Rechen-Skript setzt die neue Variable BuildVersionOfAssembly auf den Wert „11.22.33.1928“ und exportiert sie als DevOps-Variable auf die Konsole. Die Möglichkeiten dabei sind grenzenlos. Üblicherweise sollte man die BuildID nutzen, welche eine stetig steigene Nummer je getriggerten Build im CI-System ist. Problem: Die Zahl wird irgendwann größer als die erlaubten 16-bit an dieser Stelle. Lösung: Man macht Modulo 2^16 und hofft, dass dies nur alle 65k Builds zu Problemen führt und inkrementiert dann schnell die Patch-Version (Stelle 3). Wahlweise kann man auch die oberen Bits auf Patch-Version geben; dann aber nur zwei Versionsstellen vorgeben. (alles hier nicht gemacht).

Skript

Version Script (PowerShell inline):

# This script computes the $(BuildVersionOfAssembly) variable off of the User set $(VersionOfAssembly) build var.
# adds the buid pipeline ID (ordered number) as the last figure of the version

# VersionOfAssembly is expected to be "x.y.z" BuildVersionOfAssembly becomes "x.y.z.buildID"
Write-Host  "Looking for VersionOfAssembly. Found : $Env:VersionOfAssembly"

if( -not ("$Env:VersionOfAssembly" -match "^\d+\.\d+\.\d+$" ))
{
   Write-Host  "##vso[task.LogIssue type=error;]Error: $Env:VersionOfAssembly does not match X.Y.Z format."
   exit 1
}

$BuildVersionOfAssembly = $Env:VersionOfAssembly + '.' + $Env:Build_BuildId
Write-Host  "BuildVersionOfAssembly= $BuildVersionOfAssembly"

#To set a variable in DevOps pipeline, use Write-Host
Write-Host "##vso[task.setvariable variable=BuildVersionOfAssembly]$BuildVersionOfAssembly"
Version Script in DevOps – Variablen Sektionen leer lassen

Version in Assemblies schreiben

Versionen sind als Varialben da, doch wie bekommen wir sie als Assembly-Attribute in die Assemblies hinein. MSBUILD hat das etwas unbekannte Merkmal ‚directory target‘. Dieses sind Kompile-Schritte, die MSBUILD pro Projekt durchführt. Setzt man sie richtig ein, werden diese Aufgaben für jedes Projekt im selben oder tieferen Verzeichnis ausgeführt. Siehe .

Das Skript muss also irgendwo ‚oben‘ im Projekt angelegt werden und wir nennen es ‚Directory.Build.targets‘. Innerhalb des Skripts kann sogar inline-C# verwendet werden. Dabei nutzen wir Reguläre Ausdrücke um die AssemblyInfo.cs-Dateien zu patchen (alle 3 Versionsattribute). Dabei wird aber nicht die eigentliche Datei gepatcht, sondern eine Kopie. Die Originale (im VCS eingecheckt) werden als ‚targets‘ ausgeschlossen und dafür die gepatchten hinzugefügt. Das ist besser als checkout+checkin auf Build- und Entwicklermaschinen. Denn diese Targets werden ja immer mit ausgeführt. Wenn aber BuildVersionOfAssembly leer ist, macht dieses ‚Target‘ nichts. Das Ganze sollte mit *.vb genauso funktionieren.

Script: \Directory.Build.targets

<Project DefaultTargets="Build" InitialTargets="UpdateAssemblyVersion" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- from:
    http://www.lionhack.com/2014/02/13/msbuild-override-assembly-version/
    Creates modified version of AssemblyInfo.cs, replaces
    [AssemblyVersion] attribute with the one specifying actual build version
    (from MSBuild properties), and includes that file instead of the
    original AssemblyInfo.cs in the compilation.

    Works with both, .cs and .vb version of the AssemblyInfo file, meaning
    it supports C# and VB.Net projects simultaneously.

    see:
    Global .NET Versioning Strategy – AssemblyInformationalVersion
    or https://intovsts.net/2015/08/24/tfs-build-2015-and-versioning/ for
    further information

    Note: C++ projects have a CommonCpp.targets
-->

  <Target Name="UpdateAssemblyVersion" BeforeTargets="Compile"
       Condition="'$(VersionOfAssembly)' != '' or '$(BuildVersionOfAssembly)' != ''">
    <!-- Find AssemblyInfo.cs or AssemblyInfo.vb in the "Compile"
        Items. Remove it from "Compile" Items because we will use a modified
        version instead. -->
    <ItemGroup>
      <OriginalAssemblyInfo Include="@(Compile)" Condition="%(Filename) == 'AssemblyInfo' And (%(Extension) == '.vb' Or %(Extension) == '.cs')" />
      <Compile Remove="**/AssemblyInfo.vb" />
      <Compile Remove="**/AssemblyInfo.cs" />
    </ItemGroup>
    <!--  Copy the original AssemblyInfo.cs/.vb to obj\ folder, i.e.
          $(IntermediateOutputPath). The copied filepath is saved into
          @(ModifiedAssemblyInfo) Item. -->
    <Copy SourceFiles="@(OriginalAssemblyInfo)"
          DestinationFiles="@(OriginalAssemblyInfo->'$(IntermediateOutputPath)%(Identity)_patched')">
      <Output TaskParameter="DestinationFiles" ItemName="ModifiedAssemblyInfo"/>
    </Copy>

    <!-- DON'T Use VersionAssembly if InfoVersionAssembly is empty -->
    <!-- <PropertyGroup> -->
    <!-- <InfoVersionAssembly Condition="'$(BuildVersionOfAssembly)'== ''">$(VersionOfAssembly)</InfoVersionAssembly> -->
    <!-- </PropertyGroup> -->

    <Message  Text="-------------------------------------------------------------------------------" Importance="high" />
    <Message Text="Setting AssemblyVersionAttribute to $(VersionOfAssembly) " Importance="high" />
    <Message Text="Setting AssemblyFileVersionAttribute to $(BuildVersionOfAssembly) " Importance="high" />
    <Message Text="Setting InfoVersionAssemblyAttribute to $(VersionOfAssembly) " Importance="high" />
    <Message Text="Temp file is %(ModifiedAssemblyInfo.FullPath) " Importance="high" />
    <Message Text="--------------------------------------------------------------------------------" Importance="high" />

    <!-- Replace the version bit (in AssemblyVersion and
        AssemblyFileVersion attributes) using regular expression. Use the
        defined property: $(VersionOfAssembly). -->

    <!-- TODO: For Relseases, AssemblyVersion should be set to InfoVersionAssembly -->
    <Message Text="Setting AssemblyVersion to $(VersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)" 
                     Condition="'$(VersionOfAssembly)' != ''"
                     Regex="AssemblyVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyVersion("$(VersionOfAssembly)")" 
                     />
    <Message Text="Setting AssemblyFileVersion to $(BuildVersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                     Condition="'$(VersionBuildVersionOfAssemblyOfAssembly)' != ''"
                     Regex="AssemblyFileVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyFileVersion("$(BuildVersionOfAssembly)")"
                     />
    <Message Text="Setting InfoVersionAssembly to $(VersionOfAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                     Condition="'$(VersionOfAssembly)' != ''"
                     Regex="AssemblyInformationalVersion(Attribute)?\("(\d+)\.(\d+)\..*"\)"
                     ReplacementText="AssemblyInformationalVersion("$(VersionOfAssembly)")"
                     />
    <!-- Include the modified AssemblyInfo.cs/.vb file in "Compile" items (instead of the original). -->
    <ItemGroup>
      <Compile Include="@(ModifiedAssemblyInfo)" />
    </ItemGroup>
  </Target>

  <UsingTask TaskName="RegexUpdateFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Regex ParameterType="System.String" Required="true" />
      <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Using Namespace="Microsoft.Build.Framework" />
      <Using Namespace="Microsoft.Build.Utilities" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
try {
var rx = new
System.Text.RegularExpressions.Regex(this.Regex);
for (int i = 0; i < Files.Length; ++i)
{
var path = Files[i].GetMetadata("FullPath");
if (!File.Exists(path)) continue;

var txt = File.ReadAllText(path);
txt = "// <auto-generated />\r\n" +
rx.Replace(txt, this.ReplacementText);
File.WriteAllText(path, txt);
}
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
      </Code>
    </Task>
  </UsingTask>

  <!-- ConsoleToMsBuild="True" IgnoreStandardErrorWarningFormat="true" IgnoreExitCode="True" -->
  <!-- MsBuild is probably the only build tool in the world that can't copy dependencies without help.-->
  <!-- Therefore, I can't recommend msbuild for professional use. -->
  <!-- Add this lines to your project file:
  <PropertyGroup
  Condition="Exists('$(SolutionDir)CommonSettings.targets')">
  <BuildDependsOn>
  $(BuildDependsOn);
  CopyLocalAgain;
  </BuildDependsOn>
  </PropertyGroup>
  -->
  <Target Name="CopyLocalAgain">
    <!-- Bug with incremental clean should be fixed. -->
    <!-- <CreateItem Include="%(Reference.HintPath)" -->
    <!-- Condition="'%(Reference.Private)'=='true'"> -->
    <!-- ItemName defines the name of the item list. Who came up with this syntax? -->
    <!-- <Output TaskParameter="Include" -->
    <!-- ItemName="_CopyLocalReferencesAgain"/> -->
    <!-- </CreateItem> -->
    <!-- <Message Text="Copy CopyLocal references again: @(_CopyLocalReferencesAgain)" /> -->
    <!-- <Copy SourceFiles="@(_CopyLocalReferencesAgain)" -->
    <!-- DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true"> -->
    <!-- </Copy> -->
  </Target>
</Project>

MSI und Bootstrapper Setups

Die MSI Setups (WIX) und WIX-Bootstrapper-Setups funktionieren ein wenig anders um ihre Versionsnummer eingeimpft zu bekommen. Da es aber MSBUILD-Projekte sind, kann man ähnlich vorgehen. Quelle der Inspiration: Dieser Stackoverflow Artiel.

  1. In der Projektdatei *.wxiproj muss dieser Teil relativ an den Anfang. Hier wird die Umgebungsvariable als ‚Version‘ genommen. Bei fehlender Variable gibt es einen Fallback. Auf diese Weise gibt es immer eine gesetzte Version. Ansonsten scheitert der WIX-Build.
    <!-- define version constant if CI-Build not running  use a default -->
    <Version Condition=" '$(BuildVersionOfAssembly)' == '' ">5.99.99</Version>
    <Version Condition=" '$(BuildVersionOfAssembly)' != '' ">$(BuildVersionOfAssembly)</Version>
    <DefineConstants>Version=$(Version)</DefineConstants>
  1. In die Dateien *.wxs nutzt man die soeben gesetzte Variable (Verfpgbar als $(ver.Version)).

Beispielausschnitt einer *.wxs:

  <Product Id="CDE4C223-7A78-45FF-A984-675F414BD516" Name="!(loc.PRODUCT)" Language="1033" Version="$(var.Version)" Manufacturer="!(loc.MANUFACTURER)" UpgradeCode="cae20b70-d924-4b23-b90e-9ea1b5f5026b">
    <Package InstallerVersion="200" Manufacturer="!(loc.MANUFACTURER)" Compressed="yes" Description="!(loc.PRODUCT) $(var.Version)" />
    <Property Id="PREVIOUSVERSIONSINSTALLED" Secure="yes" />
    <Property Id="TAKEBACKUP" Secure="yes" />
    <Upgrade Id="cae20b70-d924-4b23-b90e-9ea1b5f5026b">
      <UpgradeVersion Minimum="$(var.Version)" IncludeMinimum="no" OnlyDetect="yes" Language="1033" Property="NEWERPRODUCTFOUND" />
      <UpgradeVersion Minimum="0.79.0" IncludeMinimum="yes" IncludeMaximum="no" Maximum="$(var.Version)" Language="1033" Property="UPGRADEFOUND" />
    </Upgrade>

Release

Zu einer Build-Pipeline gehört immer auch eine Release-Pipeline. Die soll aber nicht das Thema dieses Betrags werden. Hier ging es vornehmlich um das korrekte Errechnen und setzen von Assembly-Versionen und MSI-Versionen.

Als Bemerkunt oder Tip sei hier nur gesagt: Eine Build-Pipeline erzeugt Artefakte (irgendwo in Azure in einem Storage). Diese kann man im DevOps zwar herunterladen und damit arbeiten. Aber die verschwinden irgendwann. Daher benötigt man eine Release-Pipeline. Damit macht man eigentlich eher ‚Deployments‘ und Tests von Software. Man kann aber auch einfach eine leere Pipeline anlegen und einfach nur einen Copy-Task anlegen. Wenn der passende Agent ‚inhouse‘ läuft, kann man dann die Buildartefakte auf einen netzlokalen SMB-Share kopieren.

Fazit

Wir haben hier gesehen, wie man dynamisch beim Build eine jeweils neue Version errechnet und damit eine DevOps-Pipeline-Variable setzt und diese dann erfolgreich in die Buildartefakte (Assemblies und MSIs) hineinbekommt. Dies soll eine Inspiration sein, um jederzeit korrekt versionierte Builds zu haben und MSI-Installer, die automatische Upgrades machen können.

Multi Document Interface mit Angular

Problemstellung

Es geht in diesem Beitrag um eine Möglichkeit, in einer ASP.Net+Angular-Anwendung mehrere Dokumente und die Standardnavigation darzustellen. Man stelle sich vor, es wird eine Business-Anwendung in Angular entwickelt und diese kann z.B. Akten anzeigen. Zusätzlich hat sie aber, wie jede vernünftige Anwendung, eine Navigation über diverse Bereiche. Was man also will, ist: Es gehen für die Akten eigene Tabs auf und in einem „Haupt-Tab“ wird weiterhin die übliche Navigation durchgeführt (z.b. Profileinstellungen, andere Aspekte der Anwendung). Hier zeige ich das Beispielhaft an einer von mir entwickelten ASP.Net + Angular- Anwendung (auf Basis von ABP). Es sollte sich aber leicht auf alle Angular-Anwendungen übertragen lassen.

Router

Kurze Rekapitulation, wie man in Angular Navigiert. Die Anwendung wird als eine Hierarchie von Komponenten (components) definiert. Typischerweise mit *.html*-Dateien. Irgendwo befindet sich darin ein Element *<router-outlet></router-outlet>*. Hierdurch weiß der *Router* von Angular, dass an dieser Stelle die Komponenten des Routing-Ziels eingeblendet werden sollen. Was dann auch passiert.

Tatsächlich kann man mehrere *router-outlet*s definieren. Die Weiteren müssen individuelle Namen haben. Dann kann man mit einer tollen URL-Syntax mit Doppelpunkten und Outlet-Namen für alle Outlets gleichzeitig verschiedene Komponenten „ernavigieren“. Allerdings hat sich das meinen Kenntnissen und meiner Vorstellungskraft entzogen… Also ob man nur einzelne Outlets ändern kann, derweil der Rest bleibt und wie man dann dynamisch mehr und mehr solche Outlets erstellt (die Tabs) und auch wie man sie sinnvollerweise dann benennt. Also dieser Ansatz wurde ad Acta gelegt.

Der Ansatz

Datenhaltung

Wie sollte das Ganze sinnvoll gestaltet werden? Die Idee wäre, dass man ein Objekt hat, welches die Tabs als Datenstruktur enthält und einige Methoden bietet, um die Tabs zu verwalten. Ich habe es als Singleton realisiert. Es kann aber, je nach Architektur, auch sinnvoll sein, mehrere davon zu haben. Z.B. wenn man mehrere Tabs aufbauen will. Dann muss aber irgendwie das „default“-Router-Otulet abschaltbar werden.

Wir brauchen auf alle Fälle einen injezierbaren Service (@Injectable), der in den verschiedensten Modulen der Anwendung verfügbar ist und die Tabs programmatisch modifizieren kann. Zusätzlich muss er aber auch für die Darstellungskomponente verfügbar sein.

Code von src\app\shared\layout\open-document-service.ts:

import { Injectable,
    Inject,
    Type,
    ReflectiveInjector,
    ViewContainerRef, 
    Injector } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';

// Injezierbarer Service, um auf die globale Definition
// der Tabs zuzugreifen und zu manipulieren
@Injectable()
export class OpenDocumentService {
  
    constructor(private _inj : Injector)
    {
    }

    offeneDokumente: Array<Dokument> = [  ];

    addTab(d:Dokument){
        let newId = d.id;
        const i = this.offeneDokumente.findIndex( elem=>elem.id == newId);
        if(i > -1)
        {
            // Tab aktivieren
            this.offeneDokumente[i].active = true;
            return;
        }

        // sonst kann es hinzukommen
        this.offeneDokumente.push(d);
    }

    // remove a tab
    remove(id: string) {
        const i = this.offeneDokumente.findIndex( elem=>elem.id == id);
        this.offeneDokumente.splice(i, 1);
    }
}

export class Dokument {
    public titel : string;
    public id: string;
    public removable : boolean;
    public disabled : boolean;
    public active : boolean;
    public compref: any;
}

Hier werden die Tabs in einem TS-Array gehalten und es gibt simple und eventuell verbesserbare Methoden zum Hinzufügen und entfernen. Als Struktur wurde die neue Datenklasse Dokument eingeführt, die eien Tab repräsentiert.

Darstellung

Das Ganze muss natürlich irgendwie Dargestellt werden. Zunächst steht das Control, das zur Realisierung genommen wird. Dabei habe ich mich für TabsetComponent von „ngx-bootstrap“ entschieden. Eingebunden wird das über eine neu zu erstellende Komponente in die Anwendung. Nennen wir das Teilchen mal *multdoc-control*. Im Prinzip ist diese Komponente nichts anderes als eine geschickte Tab-Definition mit ngFor. Der immer vorhandene Tab ist das „default“ *router-otulet*; und der Rest ist die per ngFor definierte Magie, mit der die offenen Dokumente/Akten dargestellt werden.

Code von src\app\shared\layout\multi-document.component.ts:

import { Component, ViewContainerRef, ViewChild } from "@angular/core";
import { OpenDocumentService, Dokument } from "./open-document-service";
import { ContentPresenterComponent } from "./content-presenter.component";
import { TabsetComponent } from "ngx-bootstrap";

// Das ist so ziemlich nur eine GUI (html zusammenfassung) Komp.
// https://valor-software.com/ngx-bootstrap/#/tabs
@Component({
    template: `  
    <div>
        <tabset #mainTabs>
            <tab heading="Titel">
                <router-outlet (activate)='onOutletActivate($event)'></router-outlet>
            </tab>

            <tab *ngFor="let tabz of offeneTabs.offeneDokumente"
                [heading]="tabz.titel"
                [active]="tabz.active"
                (selectTab)="tabz.active = true"
                (deselect)="tabz.active = false"
                [disabled]="tabz.disabled"
                [removable]="tabz.removable"
                (removed)="removeTabHandler(tabz)"
                [customClass]="tabz.customClass">
                <content-presenter [component]="tabz.compref"></content-presenter>
            </tab>
        </tabset>
    </div>`,
    selector: 'multi-document-component',
    providers: [
        OpenDocumentService,
    ],
    viewProviders: [ ContentPresenterComponent ],
})
export class MultiDocumentComponent  {

    @ViewChild('mainTabs') mainTabs: TabsetComponent;
    offeneTabs: OpenDocumentService

    constructor(private _vcr: ViewContainerRef,
         offeneTabs : OpenDocumentService)
    {
        this.offeneTabs = offeneTabs;
    }
     
    removeTabHandler(tab:Dokument){
        //alert("REM: tabid " + tab.id);
        this.offeneTabs.remove(tab.id);
    }
    
    onOutletActivate(ev): void {
        this.mainTabs.tabs[0].active = true;
    }
}

Hier ist also relativ geradelinig das router-outlet und die dynamische Definition der weiteren Tabs. Da so wenig HTML dabei ist, ist alles in eine .ts-Datei gewandert. Dazu noch einige Events, wie z.B. dass bei Start der erste Tab mit dem router-outlet aktiviert wird. Nur, wie werden diese Tabinhalte Dargestellt und on-demand erstellt? Also erst bei Tab-Klick die korrekte Komponente gefunden und eingefügt? Die Lösung ist mein selbst entwickelter Contentpresenter. Hier sieht man recht klar meine Herkunft aus der WPF-Welt. Die hat mir dabei durchaus genutzt, vielleicht hätte es aber auch eine direktere Angular-Lösung gegeben.

Code von src\app\shared\layout\multi-document.component.ts:

import { Component, OnInit, 
    ViewContainerRef, AfterViewInit, ViewChild, 
    Input, 
    ComponentFactoryResolver, Inject, ViewRef, OnDestroy } from "@angular/core";
import { OpenDocumentService } from "./open-document-service";

// Komp. wie ein ContentPresenter
// https://medium.com/front-end-weekly/dynamically-add-components-to-the-dom-with-angular-71b0cb535286
@Component({
    template: `<ng-template #dynamic></ng-template>`,
    selector: 'content-presenter',
    providers: [
        OpenDocumentService,
    ]
})
export class ContentPresenterComponent implements AfterViewInit, OnDestroy   {
    @ViewChild('dynamic', { 
        read: ViewContainerRef 
      }) viewContainerRef: ViewContainerRef

    // This property is bound using its original name.
    @Input() component: any;
    factoryResolver: ComponentFactoryResolver;

    constructor(
          @Inject(ComponentFactoryResolver) factoryResolver: ComponentFactoryResolver)
    {
        this.factoryResolver = factoryResolver;
    }

    ngAfterViewInit() {
        let view: ViewRef = this.component.hostView
        this.viewContainerRef.clear();
        this.viewContainerRef.insert(view);
        view.detectChanges();
    }

    ngOnDestroy(): void {
    }
}

In ngAfterViewInit() passiert die Magie. Generell wird eine Komponente im gewählten Tab-Control erst initialisiert, wenn sie angezeigt wird. Das ist aber eine Eigenschaft des Tab-Controls. Dann aber wird die eigentliche Zielkomponente, wie sie in compref übergeben wird, einfach nur an dieser Stelle eingefügt. Sicherheitshalber wird vorher der alte Inhalt gelöscht. Zuvor muss aber der Tab-Inhalt von irgendwem erzeugt worden sein. Dazu wird die passende „Factory“ über den ComponentFactoryResolver (angular Komponente) gesucht und die Komponente erstellt. Siehe dazu den Teil Verwendung. Wichtigster Teil hier ist die Angular-Komponente <ng-temlpate>. Sie kann im TS-Code gefunden werden und mit beliebigen anderen Views gefüllt werden.

Test

Jetzt haben wir alle Teile zusammen und können alles zusammenfügen und ausprobieren. Zum Test bietet es sich an, sich eine kleine Testkomponente zu erstellen, die möglichst wenige Abhängigkeiten hat. Hier ist ein Beispiel dafür:

import { Component, OnInit, Input, OnDestroy } from "@angular/core";

// eine Testkomponente
@Component({
    template: `  
    <div>
        Ich bin ein Test von ID={{id}}
    </div>`,
    selector: 'test-component',
    viewProviders: [  ],
})
export class TestComponent implements OnInit, OnDestroy  {
    @Input() id: string;
    
    ngOnDestroy(): void {
        alert("OnDestoy TestComp:" + this.id);
    }
    
    ngOnInit(): void {
    }
}

Durch das Implementieren von OnDestroy wird die Methode ngOnDestroy() bei Entfernung aufgerufen. Das soll durch alert() veranschaulicht werden.

Verwendung

Einbindung

Die Einbindung der neuen Multi-Dokument-Komponente sieht wie folgt aus. In meinem Fall ist es in einem ABP-Theme geschehen. Grundsätzlich gilt es, alle Vorkommen von <router-outlet> durch <multi-document-component> zu ersetzen:

        ...
        <div class="m-grid__item m-grid__item--fluid m-wrapper">
            <multi-document-component></multi-document-component>
        </div>
       ...

An dieser Stelle sei erwähnt, dass bei Angular wirklich die XML-Elemente in der Form von öffnendem und schließendem Teil geschrieben werden müssen. Sonst geht es nicht.

Steuerung

Gesteuert wird die Tab-Komponente über die injezierbare Datenhalter-Instanz. Dazu muss diese zunächst als Injectable eingebunden werden und dann kann man sie zum Beispiel so verwenden:

import { Component, Injector, ViewEncapsulation, ViewChild, Input, Type, ComponentFactoryResolver, ComponentRef } from '@angular/core';
import { OpenDocumentService, Dokument } from '@app/shared/layout/open-document-service';


...

    //--Constructor-------------------------------------------------
    constructor(
        injector: Injector,
        private _fr:ComponentFactoryResolver
        ...,
        private _openDocSrv: OpenDocumentService,
    ) {
        super(injector);
    }

addAkteToView(ka: KarteiDto): void {

        const factory = this._fr.resolveComponentFactory(TestComponent);
        
        const componentref: ComponentRef<TestComponent> = factory.create(this._injector);
        componentref.instance.id = ka.id;

        let dokument:Dokument = {
            active: true,
            titel: ka.akte,
            disabled:false,
            removable:true,
            id: ka.id,
            compref: componentref,
        }
        this._openDocSrv.addTab(dokument);
    }

In dieser ‚anderen‘ Komponente wird die einzufügende Komponente (TestComponent) aufgelöst, erzeugt und eingefügt. Also nicht im Contentpresenterdaselbst, sondern hier. Dazu braucht man den Injector und die den ComponentFactoryResolver. Nach dem Resolve kann die neue Komponente angesprochen werden und vermittels einem passenden Dokument-Objekt in den OpenDocumentService eingefügt werden. Das Ergebnis ist sofort sichtbar. Die Elementvariable compref übergibt die eigentliche Komponente.

Gedanken

Ich hatte im ersten Moment natürlich schon geplant, die Instantiierung der View im Tab in diesen ContentPresenter hineinzulegen. Tatsächlich ist der Stand ja, dass die „tabanlegende“ Komponente die View instantiiert. Vorteil dieser jetzigen Vorgehensweise: Man kann die neue Komponenteninstanz, die in den Tab hineinkommt, noch weiter konfigurieren. Also an der Stelle im Code, wo das meiste Fachwissen zu dieser Komponente vorhanden ist. Würde man die Komponente im ContentPresenter instantiieren, könnte man sie nicht individuell konfigurieren – man bräuchte beispielsweise einen weiteren Callback. Diesen Weg wollte ich durchaus beschreiten, bin aber letztendlich ein wenig vor der höheren Komplexität zurückgeschreckt (war bei dieser Lösung auch nicht nötig) und ich habe es syntaktisch nicht hinbekommen, den generischen Typ ComponentRef<Comp> und den Typ Comp parametrisch in eine Schnittstelle zu gießen, sodass dieser Zweisatz funktionieren könnte:

const factory = this._fr.resolveComponentFactory(Comp);
const componentref: ComponentRef<Comp> = factory.create(this._injector);

Mein Stundensatz

Der Stundensatz eines externen Mitarbeiters und was darunter zu verstehen ist.

Häufiger bin ich als Informatiker/Softwareentwickler als Experte, Entwickler oder Berater bei Firmen unterwegs. Da in der Softwareentwicklung immer mit unerwareten Problemen zu rechnen ist, diese aber in den Risikobereich des Auftraggebers fallen, wird in der Regel mit Stundensätzen abgerechnet. Pauschale gesamtkostenschätzungen sind einfach unseriös…

Doch wann beginnt die Uhr zu ticken, und was gehört dazu. Hier wiedergegeben ist meine Meinung zu Das Ganze. Vielleicht einen Tacken großzügiger als man meint, aber ich begründe es:

  • Anfahrt nicht dabei – Die Anreise, wie auch die Abreise ist Teil der Spesen. Entweder man vereinbart, dass Fahrten extra berechnet werden, oder man macht einen „inklusiven Stundensatz“, der Spesen enthält, und rechnet sich das intern hin.
  • Zeit beginnt ab Ankunft am PC – auch wenn es dann vielleicht erst einmal einen Kaffee gibt, denn Kaffeetrinken ist auch dabei.
  • PC startklar machen/Updates, die kommen – Teil der berechneten Zeit. Dass ein Rechner hochfährt oder aus sonstigen Gründen (unsägliche Updates) nicht sofort benutzbar ist, das gehört zu den unabwendbaren Tatsachen. Zwar ist es auch nicht die Schuld des Projektgebers, aber der Berater kann es eigentlich auch nicht ausbaden – Freizeit ist es jedenfalls nicht. Wo soll man in 5min auch hin? Nebenbei lässt es sich mit einem „Produktiven Kaffee“ überbrücken.
  • Kaffee/Wasser besorgen und mit Kollegen trinken – ist essentieller Teil der Arbeit. Damit berechnete Zeit. Auf diesem sog. Flurfunk wird neben Tratsch auch häufig die nächste hilfreiche/essentielle Information verteilt. Es wäre schade für beide Seiten, das zu verpassen. Der Deal zwischen Quatsch und Prduktivität ist zugunsten letzterer. Daher als sog. „Produktiver Kaffee“ bezahlte Zeit. Ein weiterer Aspekt ist, dass Kaffee und Getränk auch für den Körper essentiell sind. Ein Entwickler mit gelöschtem Durst oder mit wohligem Gefühl ist ein besserer Entwickler. Da diese Zeiten sich mehrheitlich wieder auszahlen für den Projektgeber ist es in seinem Interesse, dass Experten sich gut versorgen können. Es muss nicht gratis sein, wäre aber bei Kosten/Nutzenrechnung im Vorteil.
  • Mittagspause – eindeutig nicht dabei. Hier geht der Experte in seine Freizeit. Die er zwar zum Essen verwendet aber es ist einfach eine Pause und die Arbeitszeit wird unterbrochen.
  • Web-Browse-Pause am Rechner – ist bezalte Zeit. Warum? Als Entwickler muss man sich stark konzentrieren, um die bestellte Leistung zu erbringen. Das geht nicht 8h am Stück. Da ist es naturlich klar, dass zu jedem Konzentrationsboost eine Zerstreuungspause gehört. Und wenn Web-Browsen der Zerstreuung dient, dann ist es produktiv. Denn nur wer sich kurz zerstreut hat, kann wieder Leistung bringen. Möglicherweise kommt man ja genau dabei auf die Lösung eines Problems. Daher sind 1/4 der Zeit als derartige Pausen als normal und produktiv einzustufen.
  • Herumlaufen/Herumstarren – ist bezahlte Zeit. Hier gilt dasselbe wie bei Web-Browse-Pause. Wenn das herumstarren der Zerstreuung oder dem Denkvorgang dient und 1/4 der Zeit nicht überschreitet, dann ist es produktive Zeit.
  • Eine Runde Kickerspielen – fraglich. Hier kann man durchaus mal die Zeit anhalten. Vor allem, wenn es länger dauert. Indes ist es auch Zerstreuung und in diesem Fall dann produktive Zeit. Kommt auf die Länge an. 1/4-Regel.
  • Klopause – überraschend, aber auch das ist bezahlte Zeit. Gut, man kann argumentieren, dies solle zu Hause gemacht werden. Andererseits kommt das Bedürfnis wie höhere Macht aus dem Nichts und irgendwen trifft es halt. Aber halt: Die besten Ideen kommen doch oft, wenn man auf dem Örtchen sitzt… und Zerstreuung ist es auch. Daher: Wenn es wieder unter die 1/4 der Zeit-Regel passt, ist auch die Sitzung bezahlte Zeit.
  • Konferenz (sog. Meeting) – das hat natürlich der Projektgeber zu verantworten und wenn er der Meinung ist, dass man bei irgend einer Konferenz oder Lehrgang dabei sein sollte, dann ist das mit dem vollen Stundensatz zu bezahlen.
  • Privates Telefonat – kommt drauf an. Auch hier gilt wieder dasselbe wie bei Zerstreuung. 1/4 der Zeit darf es sein. Wird es länger oder ist das Gespräch eher aufregend, dann gehört es aus den Berechneten Stunden heraus.
  • Ende – wenn der Rechner verlassen wird und die letzte Taste gedrückt wurde. Dann endet die verbuchte Zeit.

Zusammenfassend kann man sagen, dass ein Arbeitstag nie 100% hochkonzentriert durchgehalten werden kann. Geht gar nicht. Es muss Zerstreuung und Entspannung dazu. Diese beiden Teile sind aber zeitlich verflochten und quasi untrennbar miteinander verbunden. Wer mich 1h engangiert, muss mir 15min „unproduktive“ Zeit zugestehen. Nur so bekommt er 45min volle Leistung. Was man als Entwickler in diesem ca. 1/4 der Zeit macht, ist egal. Gut wäre, dass man sich dabei zerstreut und im Unterbewusstsein weiterdenkt. So haben alle was davon. Und weil man nicht mitten am Tag 15min Freizeit machen kann, gehört diesr Teil genauso zum engagement dazu und wird berechnet.

Unit-Tests über fremdem Code mit „Microsoft Fakes“

Ja sowas geiles. Testen von statischen Methoden, Interfaces und sammeln von Ergebnissen (Aufrufen). Das alles geht mit dem ehemaligen MS-Research-Tool „Moles“, welches heute in „Microsoft Fakes“ aufgegangen ist und seit VS2012 mitgeliefert wird. Da allerdings schon jemand einen tollen Artikel darüber geschrieben hat, verweise ich einfach darauf:

Unit-Tests mit MS-Fakes…. Links:

Böser Dispatcher

An dieser Stelle eines der vielen Gotchas bei .Net-Entwicklung.

Es gibt manchmal Klassen (oft ViewModel), die haben prim​är mal keine GUI-Referenz, werden aber manchmal wecheslweise in einem Worker-Thread ausgeführt. Problematisch wird dann eine eventuell daran gebundene GUI. Oder man muss einfach nur unterscheiden, ob man im Worker-Thread oder im GUI-Thread lebt.

Der Dispatcher ist mehr oder weniger das unterscheidbare Merkmal, an dem ein GUI-Thread von anderen unterschieden werden kann:

Gleicher Thread wie der Dispatcher… Gut, möchte man meinen. Doch diese Prüfung hat einen bösen Fehler.CurrentDispatcher hat Seiteneffekte: Existiert kein Dispatcher zum aktuellen Thread, wird ein neuer erzeugt. Dispatcher sind nämlich keineswegs GUI-Spezifisch, sondern ein allgmeiner Dispatch-Mechanismus. Der Name ist irreführend.

Korrekt ist dagegen die Prüfung mit diesem Idiom:

Doch leider zu kurz gedacht. Im Prinzip ist das Richtig. Doch nur so lange, als keiner das erstere Idiom mit Dispatcher.CurrentDispatcher verwendet hat und uns somit einen parasitären Dispatcher auf den Worker-Thread gesetzt hat. Irgendwann hat dann jeder Worker-Thread einen Dispatcher.

Also was tun? Gleich auf Application zugreifen … tja leider. Denn Applicaiton ist der einzig gute Ankerpunkt in diesem Fall.

Dabei soll aber nicht unerwähnt bleiben, dass es in einer GUI-Applikation durchaus mehrere UI-Threads geben kann. Typischerweise dann in mehreren Fenstern. Es ist unwahrscheinlich, aber es geht und dann wäre auch dieser Code hier wahrscheinlich inkorrekt.

Bei WinXP-Fotoanzeige Diashow-Intervall einstellen

Womit man auf seine alten Tage noch so zu tun bekommt. Windows XP glaubte ich ja eigentlich schon hinter mich gebracht zu haben. Doch da gibt es Leute, die es tatsächlich noch einsetzen. Es war ja auch nicht so schlecht… nur ein wenig unsicher sonst aber…

Und wer es einsetzt um damit Urlaubsfotos durchzublättern kommt auf die Diashow der Fotoanzeige. Da die Zeitintervalle zwischen den Bildern mit 3 sek nicht allen Leuten taugen, kommen dann Fragen, wie man dieses Intervall wohl ändert. Da es keine Oberfläche dafür gibt, bleibt nur die Registrierung zu bearbeiten. Konkret muss man unter HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionExplorerShellImageView gehen und dort den DWORD-Wert Timeout erstellen. Hinein kommt die Zeit in Millisekunden, die die Fotoanzeige zwischen zwei Bildern warten soll.

Aus gegebenem Anlass habe ich für diese einfache Aufgabe ein Programm geschrieben. Verwendet habe ich dazu WinForms und C#. Man wird also das .Net Framework 2.0 benötigen. Das Programm bietet eine simple Oberfläche um genau diese Eine Aufgabe zu erledigen. Gut für Leute, die häufiger mal die Zeitabstände ändern müssen, mit der Registrierung aber nix am Hut haben.

Download Programmdatei

Download Quellcode

Anmerkung: Das Programm ist nach dem QnD-Prinzip entstanden. Daher nur schnell zackzack entstanden… einfach so.

WPF Dependency Properties von innen Setzen

Entwicklung eines WPF-Composite-Controls mit Dependency Properties (aka. Abhängkgeitseigenschaften)

Ab und zu muss man bei der Etnwicklung von WPF-Oberflächen neue Controls erstellen. Nun gibt es verschiedene Arten von Controls. Man unterscheided sog. Lookless Controls und Composite Controls. Erstere sind quasi rein nur eine von UIElement abgeleitete Datenstruktur. Das gesamte Verhalten, das Aussehen und die modifikation der Datenfelder (die Dependency Properties) geschieht über Styles und dort wiederum mit Triggern und Binungen. Darum soll es hier aber nicht gehen. Hier geht es um die andere Art von Control: Um Kompositum-Kontrollelemente bzw. User-Controls. Also in Etwa ein Panel oder ein Window. Mehrere Controls sind in einem neuen Control zusammengepfärcht und interagieren intern miteinander, während sie nach außen hin wie ein einziges auftreten. Dies lässt sich mit und ohne View-Model machen. Man hat also die Wahl zwischen MVVM und code behind. Je nach komplexität des Kontrollements ist es entweder sinnvoll oder einfach Overhead, ein View-Model dazu zu bauen. Hier soll es jetzt um ein einfaches Control gehen und daher greifen wir auf code behind zurück.

Das Control

Entwickelt wird ein Datei-Auswahl-Control. Es besteht aus einem Label und einem Button. Klickt man den Button, so erscheint ein Datei-Öffnen-Dialog und die fürderhin ausgewählte Datei wird angezeigt. Gleichzeitig hat auch die extern sichtbare Eigenschaft „FileName“ ihren Wert geändert und alle Bindungen  darauf ändern sich mit. Die Wahl fällt auf ein Control mit Code-Behind. Somit können wir einfach auf das Click-Ereignis des Buttons reagieren. Dort muss dan aktiv der Wert der Eigenschaft geändert werden. Tatsächlich ist hier par Bindung das Control sein eigenes View-Model.
Bestandteile:

Vorgehen

Zunächst benötigen wir ein Control.
Daher legen wir eine XAML-Datei an und passend dazu eine Code-Behind-Datei. Wir erben von System.Windows.Control.

 

und

 

Controlaufbau

Das Control besteht aus zwei weiteren Controls: Button und Label. Wir fügen beide ein und Binden das Label an die noch zu erstellende Eigenschaft FileName. Damit das nacher funktioniert, muss die Source noch korrekt sein. Wir erreichen das recht einfach, indem wir dem Conttol(!) den DataContext setzen und auf sich selbs verweisen lassen. Das Control ist
so gesehen sein eigenes View-Model.

 

Abhängigkeitseigenschaften

Zur erfolgreichen Bindung benötigt das Control noch eine Dependency Property FileName:

mah beachte, wie per FrameworPropertyMetadata ein Standardwert mitgegeben wurde und die Bindungsoptionen standardmäßgig auf TwoWay definiert wurden. Dies hat im Folgenden den Vorteil, dass man von Extern (bei Verwendung) nicht bei Binungen Mode=TwoWay angeben muss.

Events

Wir wollten es einfacher mit dem Button. Nun fehlt noch der Eventhandler:

Man beachte hier die beiden Aufrufe der von DependencyObject stammenden Methodena: SetCurrenValue und ClearValue. Damit wird der Wert bzw. die Bindung hinter einem Dependency-Property geänder bzw. auf den Std.-Wert (aus den Metadaten) zurückgesetztgesetzt. Verwendet man vergleichsweise dazu GetValue/SetValue wie in der Implementierung des CLR-Properties, zerstört man die Bindung. Das wäre fatal, da dann die Funktionalität zusammenbricht. An dieser Stelle sein noch kurz auf die Doku verwiesen… Demnach seien CLR-Getter/Setter nur so zu implementieren, wie hier gezeigt. Nur Aufrufe von GetValue/SetValue und keine weiteren Aktionen nebenher. Denn WPF ruft gerne selbst GetValue/SetValue mit passenden Parametern auf und umgeht dabei die CLR-Properties. Zusatzaktionen
muss man daher in passenden PropertyChanged– bzw. CoerceValue– oder ValidateValue-Callbacks machen. Auf selbige wurde hier verzichtet.

Eingebaut

Nun sehen wir uns noch an, wie dieses Control zu verwenden ist. Dazu wurde ein WPF-Fenster gestaltet, dass dieses Control verwendet und gleichzeitig einen Textblock an unsere neue Dependency-Property bindet:

 

… es ist kein Code-Behind nötig….
Man beachte, wie zunächst der text „aus window“ im Control steht, und später der Wert aus dem Eventhandler des Controls (siehe im Code: cancel oder OK-Zweig).

Programmiersprachen und Metadaten

Schaut man sich heute mal so den Quelltext eines mittleren Programms an, welches in C, C++ oder ähnlich geschrieben ist, wird man feststellen, dass es von Metadaten und Stringlisten nur so wimmelt. Der Grund ist klar die komplexität des Programms. Verursacht durch die Anforderungen die nach Flexibilität verlangen. Vereinfacht gesagt reicht eine Enumeration als Typ nicht mehr aus – stattdessen wird zusätzlich oder ersatzweise eine Liste oder ein Feld mit Zeichenketten und oder Konstanten angelegt. Oder es werden zusätzliche Elementvariablen in Klassen oder Strukturen eingeführt, die Metadaten zu ihren Objekten halten oder oder oder.

Sieht man sich dagegen ein Programm in einer verwalteten Sprache an wie Java oder .Net, stellt man ähnliches fest aber weit weniger. Meist sind die Programme komplexer geworden, weil nun mehr Freiheiten bestehen. Aber eigentlich benötigen sie derartiger Hilfskonstruktionen weniger. Stattdessen wird Reflexion häufiger eingesetzt. Typen (z.B. eine Enumeration) werden einfach definiert und verwendet (klassisch, Lehrbuch) und wenn die Anforderungen komplexer werden, werden diese Typen reflektiert. Man verlässt sich auf die von der Laufzeitumgebung bereitgestellten Metadaten und inspiziert sie / verwendet sie passend.

Das Resumé aus dem Ganzen ist nun: Früher, in den kompilierten Sprachen gab es wenig bis keine Metadaten. Ergo führen viele Programme im Quelltext welche ein (die aber nur eher schlecht mit den eigentlichen Daten (Kode) integrieren). Man stellte also fest, dass quasi jedes etwas komplexere Programm solche Metadaten benötigt. In der Folge berücksichtigte man das bei der Entwicklung von verwalteten Sprachen und sagte sich: Lass uns gleich für alle Typen zwangsweise Metadaten einführen und mitschleppen. So ist es bei Java, C# und .Net also möglich, Reflexion zu verwenden und Daten über Typen zu ermitteln. Überdies erlauben beide Sprachen das Anfügen von zusätzlichen Metadaten in Form von Attributen bzw. Annotationen. Das hat letztlich zu einem neuen Stil in der Entwicklung geführt. Man verlässt sich nun mehr auf Metadaten im Typsystem und legt Dinge generischer aus. Zwar werden Programme dadurch langsamer aber auch flexibler, stabiler und besser wartbar.

Über die Möglichkeit, die das Generieren neuer Typen zur Laufzeit angeht, lasse ich mich ein andermal aus.