Ich auf dem rc3 (CCC)

Ein Vortrag war das Elektrogruselkabinett Indien-Edition

Die Bilder können gerne nochmal angesehen und auch verwendet werden unter einer liberalen CC-BY-Lizenz. Siehe: Album


(Dieses Werk ist lizenziert unter einer Creative Commons Namensnennung – Weitergabe unter gleichen Bedingungen 4.0 International Lizenz.) – Robert Köpferl

Der zweite Vortrag ist How to digitale Barrierefreiheit.

Präsentation dazu: Runterlad

Beides auf der #chaoszone im #rc3

Das Programm wurde wieder einfacher…

Das ist es, was uns regelmäßig verkauft wird. Ob App, Webseite, Programm oder andere Oberfläche. Von Version zu Version wird alles immer einfacher und besser bedienbar (… und doch immer fetter auf Platte und Arbeitsspeicher).

Besser bedienbar und einfacher

Tja, da muss man sich (und andere?) wirklich mal fragen, ob die Entwickler und Designer vor Jahren alle so doof waren oder überall Nerds in den UX-Workshops unterrichtet haben. Denn mit dieser Version wird alles viel besser und vor allem einfacher. Oder doch nicht?

Wirklich?

Was die da unter einfacher und besser bedienbar verkaufen ist in Wirklichkeit eine Reduktion der Interaktionselemente und der Information. Wo es früher drei Buttons gab, ist jetzt nur mehr einer. Die anderen zwei Funktionen sind meist nicht einmal migriert (z.B. durch lange klicken, was eine Komplizierung wäre), sondern gestrichen. Wo früher Toolbars waren ist heute gähnende Leere und dafür auf dem Fenstertitel ein kleines Knöpflein – für den Ganzen Rest. Weil wir ja alle immer Vollbild arbeiten wollen und uns dabei jah nichts im Weg sein darf… Pulldown-Menüs werden ausgedünnt oder verschwinden gleich ganz. Die kompakte Befehlsdarstellung ist halt so was von 90er Jahre.

Konsequenz … komplizierter

Das Ergebnis ist doch, dass eine Software nicht mehr so flexibel bedienbar ist. Sie kann zwar jetzt den Standardfall einfacher abarbeiten, da für viele Parameter Standardwerte angenommen werden und die spezielleren Aufgaben nicht mehr erreichbar sind. Aber für die (früher normalen) Spezialfälle ist alles komplizierter geworden. Der Experte wird also seine helle Freude daran haben, weil er jetzt im hintersten Menü erst einmal die Funktion wiederfinden muss – falls noch vorhanden.

Es ist ja so, dass die Software an sich komplex ist, weil sie ein komplexes Problem löst. Das ist die Abstraktion eines Problems in seinen Facetten. Wenn ich jedoch die Komplexität aus der Nutzerschnittstelle herausnehme, kann ich mein Problem gegenüber der Software nicht mehr ausdrücken. Oder es wird umständlich.

Doch lieber nicht ‚einfacher‘

Daher mein Plädoyer: Macht die Software, Apps und Oberflächen bitte nicht immer weiter einfacher sondern macht sie komplexer! Komplexere Bedienung erlaubt mir den Problemraum zu erfassen und mich auszudrücken. Ich sehe, welche Parameter ich verändern kann und kann mir das überlegen. Auf jeden Fall sollten aber gute Standardwerte angenommen werden.

Kandidaten

Die typischen Kandidaten für solcherlei Verhalten sind alle Apple-Inspirierten Macher. Dazu gehört u.a. auch Gnome. Dort gilt das Prinzip, dass man möglichst wenig Bedienelemente zur Verfügung stellt, um blos keinen Menschen zu irritieren. Ergo nervt so eine Software damit, dass sie nicht ohne spezielle Editoren oder Konfig-Datei-Änderungen anpassbar ist. Diese Software ist unterm Strich also KOMPLIZIERTER.

Corona-Kürbis

Der Corona-Warnkürbis spricht..

.. zu uns. Was hat er uns mitzuteilen?

Corona-Warnkürbis

Das Virus SARS-CoV-2 ist auf dem Vormarsch. Sogar im sicher geglaubten Deutschland. Seid also auf der Hut und nutzt Masken wann immer es eng wird. Wichtigste Maßnahme ist das Durchbrechen von Infektionsketten. Das Virus ist eigentlich ein armer Schlucker und immer auf sein Wirtstier angewiesen und auf Zufälle bei der Übertragung von Wirt zu Wirt. Was wir machen können, ist, dem Virus diesen an sich schon schweren Infektionsweg weiter zu erschweren. Das geht am einfachsten durch das Tragen von Masken. Daher: Tut es und bleibt gesund.

Insbesondere gilt das auch mit Leuten, die man kennt! Es fühlt sich vielleicht komisch oder gar falsch an, bei Freunden/Bekannten Maske zu tragen. Bzw. es fühlt sich vertraut an, genau das nicht zu tun…. ‚man kennt sich ja…‘ Doch das ist ein Trugschluss. Das Virus unterscheidet nicht fremd und bekannt. Es breitet sich auch unter Freunden aus… hemmungslos. Daher ist auch hier Vorsicht die Mutter der Porzellankiste. Körperliche Distanz und Masken angezeigt.

Ebenfalls besonders gefährdet sind alle Menschen vom Lande und von den weniger Nicht-Hotspot-Städten (v.a. im Osten der Republik). Warum? Es war doch in der letzten Corona-Saison (Feb-Apr 2020) eigentlich eh eher ruhig und die Gefahr wurde ja quasi überschätzt?
Das ist wahr, aber genau darin verbirgt sich die Gefahr. Gerade der Sommer (eine scheiß Zeit für ein Coronavirus) war ja locker leicht, es gab Erleichterungen und die Infektionsrate war superniedrig. Doch in dieser Saison ist alles anders.!!!
In dieser Grippesaison (Okt 20-Mär 21) wird es schlimm. Schlimm! Verhindert also bitte das Schlimmste!
Das Virus konnte sich den Sommer lang quasi weltweit gut verteilen. Damit meine ich auch in die Kleinstädte, ins Erzgebirge und die Brandenburgische Pampas, wie auch die Alpenregionen … überall. Und dann kommt ganz klassisches Grippewetter dazu. Daraus folgt, dass es überall in der Republik Ausbrüche gibt. Anders als Feb 20, wo wirklich einzelne Herde isoliert vor kamen, ist es jetzt überall.
Dabei ist die Erfahrung von der letzten Saison sooo trügerisch. Weil man ja denkt: ‚… damals war es ja bei uns auch nicht schlimm und passiert ist ohnedies nichts… ‚. Genau deshalb sind die Leute in solchen Regionen besonders gefährdet. Weil sie sich weiterhin lax verhalten wollen und somit die Infektionsketten nicht durchbrechen, sondern in privaten Treffen sogar noch befördern. Plötzlich Maske am Arbeitsplatz… häää? Aber genau das sollte es sein. Die Menschen in den Hotspots sind zwar im Sommer laxer geworden, aber gefühlt ändert sich diese Grippesaison nicht so viel gegenüber der letzten.

Damit will ich es mit meinen Warnungen mal belassen und wünsche Euch allen eine erfolgreiches Versteckspiel gegen das Virus. Bleibt gesund!

8GB Wikipedia – ich in Australien 2006

Heute will ich mal einen kleinen Blick zurückwerfen. Anlass war eine Wikpedia-DVD, die mir irgendwo im Augenwinkel erschien. Da erinnerte ich mich, wie es mir damals ging. Ein kleiner Abriss der Geschichte:

Damals ? war Reisen noch ein wenig komplizierter, aber auch schon recht gut. Es gab auf jeden Fall nicht überall und kostengünstig mobiles Internet – nein nur teures. Aber es gab WLAN, welches dann und wann kostenlos war. Oft genug aber auch bezahlt. Händies waren noch Faustkeile mit smarten Anwandlugnen, aber noch entfernt von modernen, hochauflösenden und schlauen Wischflundern.

In dieser Zeit als stellte sich beim Reisen ernsthaft die Frage: Nimmt man einen Laptop mit, oder nicht. Laptops waren zu dieser Zeit schon recht portabel aber beim Gewicht+Volumen auch nicht ganz unbedeutend. Vor allem waren sie aber noch gefühlt eher teuer. Viele Leute haben diese Frage damals klar verneint. Preis und Praktik. Ich habe etwas gezögert, konnte mich aber dann doch dafür entscheiden, was eine sehr richtige Entscheidung war.

Gründe dafür waren, dass man ihn als Datenlager und Transfereinrichtung für den Fotoapparat nutzen konnte, damit telefonieren, gut Recherche machen, Filme darauf schauen, Texte damit schreiben und nicht zuletzt ihn als Wärmflasche benutzen konnte. Lauter Gründe und überdies wäre er auch unbenutzt älter geworden.

Wärmflasche, Waas? Ja, wenn man mal Bauchweh hat… dann muss man einfach nur eine Variante von Prime95 10min laufen lassen und man hat die beste Wärmflasche mit Zusatzfunktion. Die anderen Gründe klingen wohl sehr nachvollziehbar. Damals hatte man noch separate Fotoapparate mit gigantischen SD-Karten von 128 oder 256MB Kapazität. Die wollten ab und zu geleert werden. Aber auch Laptops werden geklaut, insofern man das Zeug entweder auf CD brennen wollte oder, wie ich, beim nächsten WLAN über die Nacht auf meinen Server ‚hoch-rsyncen‘. Das ging damals noch nicht sonderlich schnell. Telefonieren war auch so eine Sache. SIPGate war schon erfunden und sobald ich WLAN hatte, konnte ich kostengünstig bis gratis zu Hause anrufen. Derweil haben sich alle Anderen diese Telefonkarten gekauft und abtelefoniert. War auch nicht schlecht, aber anders umständlich.

Doch zum Titelthema. Den Laptop habe ich natürlich auch als Lektüre und Nachschlagewerk genutzt. Da damals internetfreie Zeiten durchaus noch die Regel waren, hatte ich mir vorausschauend einen Auszug der Wikipedia mit passendem Viewer auf den Rechner gepackt. 6,4 GB waren das damals. Das hatte ich dann auch gut genutzt, um bei jeder Gelegenheit (Orte, Sprachen, Länder) meinen qualifizierten Senf dazuzugeben.
Irgendwann auf Halbzeit war dieses Paket ‚veraltet‘ und ich wollte den neuesten Auszug der Wikipedia. Allerdings hatte die inzwischen über 8 GB. Doch so gut waren die Internetverbindungen nicht – selbst mit Torrent hat man ewig gewartet. Nicht zu vergessen: Volumenbegrenzung. Die Anbindung des australischen Kontinents war teuer, und so war auch Volumen an jedem australischen Internetanschluss teuer / begrenzt. Aber ich habe irgendwann an der Gold coast zu arbeiten begonnen und in einem der dortigen IT-Unternehmen konnte ich dann einen USB-Stick damit vollladen. Thema erldigt.

Tja.. manchmal ist es auch schön, von einem IT-Standpunkt in seinen Erinnerungen zu schwelgen.

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.

Die Bodenblicktheorie

Kurz

Es ist die Theorie darüber, dass 30% eines menschlichen Blickfelds immer den Boden erfasst. Daher kann eine Straßenszene komplett anders aussehen, je nach dem, wie der Boden gestaltet ist. Anders gesagt: Ein Ort kann durch Umgestaltung des Bodens unglaublich verbessert werden.

Worum es geht

In dieser Theorie geht es darum, wie sehr ein Stadtbild und der Eindruck, den ein Individuum davon hat, vom Boden abhängt. Also gar nicht mal so sehr von den Häusern und deren Fassaden, sondern mehr vom Boden. Dabei ist der Boden eher so ein Ding, das mit Füßen getreten wird und auf dem so mancher auch sein Geschäft macht. Also ein eher weniger beachteter Teil. Dieser Beitrag möchte dem darauf aufmerksam machen, dass der Boden z.B. auch in der Stadtentwicklung einen ganz wesentlichen Beitrag zum Gesamteindruck beiträgt.

Theorie der zerbrochenen Fensterscheibe

Vermutlich bekannt, sonst nachlesen. Im Grunde zieht kaputt mehr Zerstörung nach sich. Daher sofort Unrat und Defekte beseitigen. So ziehen z.B. schlecht gepflegte, inhomogene und geflickte Straßen auch Hundekot und Müll magisch an.

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.

Rechtsanwaltskanzleien – wenn die wie Frisörsalons heißen

Was wäre, wen Rechtsanwälte genauso lustige oder fantasievolle Namen hätten, wie Frisörsalons

  • Kanzlei Recht & Beuger
  • Kanzlei Paragraph & Reiter
  • Kanzlei Folgen & Schätzle
  • Kanzlei Rechthaber
  • Kanzlei Rechtsverrdreher
  • Kanzlei Berat + Schlafgut
  • Partnerschaft Hauer & Stecher
  • Kanzlei Pechsträhne
  • Kanzlei Glücksträhne
  • Syndikat Hauser, Fried & Bruch
  • Law & Ordner
  • Kanzlei Letz & Akten
  • Kanzlei Haber & Guth
  • Die Rechtsverdreher
  •  
Karl-Marx-Monument Chemnitz

Geringes Gehalt macht abhängig und fleißig

Tja, vermutlich zitiere ich, nicht gewahr Karl Marx‘ Das Kapital, gerade ein Kapitel aus demselben. Aber seit Euch gewiss, dies habe ich mir soeben selbst ausgedacht / festgestellt

These

Meine These ist also die, dass Beschäftigte, die geringe Gehälter erhalten mehr arbeiten und weniger Alternativen haben.

Hohes Gehalt

Nehmen wir als Startbeispiel einen Angestellten mit einem hohen Gehalt. Dieser kann trotz seines Lebensstils 50% seines Nettoeinkommens auf die Hohe Kante legen. Damit müsste er theoretisch nur ein halbes Jahr arbeiten und kann das andere halbe Jahr machen was er will. Zum Beispiel kann er sich auch nach einem noch höher dotierten Job umsehen oder eine Firma Gründen und richtig Millionär werden. Oder auch nur hübsch 50% seines Einkommens in Aktien investieren und selber Kapitalsit werden und von den Dividenden leben. Er wird also unabhängiger vom Arbeitskapitalismus.

Geringes Gehalt

Sehen wir uns dagegen einen Arbeiter mit geringem Gehalt an. Er muss jeden Monat malochen und jede Überstunde Mitnehmen, um sich sein Leben leisten zu können. Er gibt rund 85% seines Nettoeinkommens jeden Monat aus. Es bleibt kaum was zum Sparen und für schlechte Zeiten. Wenn mal was passiert (Thema Waschmaschine kaputt), sind die Ersparnisse im Nu wieder weg. Dieser Arbeiter wird also, gerade ob seines eher höheren Arbeitspensums, kaum Zeit haben, sich nach Jobalternativenumzusehen. Eventuell müsste er auch einen Monat Gehalt überbrücken – unmöglich. Also ist er verdammt dazu weiterhin für seinen gering zahlenden Arbeitgeber zu arbeiten. In Aktien zu investieren, geschweige denn vom Kapital zu leben und somit teil des Kapitalismuszu werden ist nur in homöopathischenDosen möglich. Faktisch also nicht.

Dieser Arbeiter wird also versuchen möglichst noch Überstunden zu machen und alles zu tun, damit er seinen Job nicht verliert. Sich unabhängig machen durch Selbständigkeit wird ihm verunmöglicht, da er keine substantiellen Ersparnisse hat.

Resumé

Das ist, wie Kapitalismus und Arbeit als Ressource funktioniert. Man hält sich den kleinen Arbeiter kurz, damit er froh ist, überhaupt irgendwas zu haben. Wenn es dann noch günstiges Schweinefleisch zu kaufen gibt (qua Politik) und Fußball, ist er sogar ein bisschen glücklich.

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.