Limit-Operations

Ein Ding, das ich bei C# schon immer und wieder vermisst habe (zumindest Damals[TM]) ist ein LimitTo.

Wie oft kommt es vor, dass man Parameterwerte oder irgendwelche Eingaben auf eine gewisse Spanne eingrenzen will. Daher habe ich mir schon lange mal die Klasse LimitOperations geschrieben, welche solche LimitTo-Erweiterungsmethoden enthält.

Aufgebaut auf IComparable klappt das mit jedem Datentyp.

Die Benutzung ist erwartungsgemäß eine neue Zuweisung. Der originäre Wert wird nicht angerührt. Geht also bestens mit Werttypen.

lvalue = lvalue.LimitTo(1,60);

Hier mal die von mir empfohlene Klasse. Ganz klassisch ohne UnitTests (uups)

Code

using System;

namespace WpfBib.Extensions
{
	/// <summary>
	/// Klasse stellt Erweiterungsmethoden bereit um an ordinalen Typen
	/// Limits einzuhalten. Also "lass das nicht größer werden als..."
	/// </summary>
	/// <example>
	/// int i = 43, j=i.LimitTo(2,44);
	/// </example>
	public static class LimitOperations
	{
		public static T LimitTo<T>(this T inValue, T lowerLimit, T upperLimit) where T : IComparable
		{
			if (inValue.CompareTo(lowerLimit) < 0)
				return lowerLimit;
			if (inValue.CompareTo(upperLimit) > 0)
				return upperLimit;
			return inValue;
		}

		public static T LimitTo<T>(this T inValue, T upperLimit) where T : IComparable
		{
			if (inValue.CompareTo(upperLimit) > 0)
				return upperLimit;
			return inValue;
		}

		public static double LimitTo_NoNan(this double inValue, double upperLimit)
		{
			if (double.IsNaN(upperLimit))
				return inValue;
			if (inValue.CompareTo(upperLimit) > 0)
				return upperLimit;
			return inValue;
		}
	}
}

Mit EF N:M-Beziehungen pflegen

Letztens wieder in einem Projekt mit Entity Framework. Da habe ich wieder eine Erkenntnis erlangt, die ich hier zu teilen versuche.

Situation

Man stelle sich vor, die Software soll eine Tabelle mit einer Detailtabelle (n:m) bearbeiten. Als Beispiel nehmen wir Produkt und Laden. Zur Einnordung: Ein Produkt kann in vielen Läden geführt werden und ein Laden führt viele Produkte. Also klassisch n:m. Das Ganze soll mit Entity Framework umgesetzt werden. Als besondere Schwierigkeit hat Produkt keine Navigationseigenschaft für die Läden. Das mag EF nicht so sehr. Passieren kann das, wenn Produkt z.B. extern zugeliefert wird. Man also keinen Einfluss auf den Code hat.

Umsetzung:

Entitäten

public class Produkt 
{
    public Produkt(Guid id, string name, decimal preis)
    {
        Id = id;
        Name = name;
        Preis = preis;
    }

    public virtual Guid Id { get; set; }

    public virtual string Name { get; set; }

    public virtual decimal Preis { get; set; }
}

public class Laden
{
    public Laden(Guid id, string name)
    {
        Id = id;
        Name = name;
        Produkte = new HashSet<Produkt>();
    }

    public Guid Id { get; }

    public virtual string Name { get; set; }

    public virtual string Inhaber { get; set; }

    public virtual ICollection<Produkt> Produkte { get; set; }
}

Laden verweist auf n Produkte, Produkte aber nicht auf Laden. Klassischerweise würde EF hier eine 1:n-Beziehung per Konvention machen. Daher ist Arbeit im Modelbuilder nötig. Hier also unser Datenkontext:

public class ErpDbContext : DbContext
{

    public DbSet<Produkt> Produkts { get; set; }

    public DbSet<Laden> Ladens { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Produkt>(b =>
        {
            b.ToTable("Produkts"); 
            b.ConfigureByConvention();
            b.HasKey(x => x.Id);
            b.Property(x => x.Name).HasColumnName(nameof(Produkt.Name)).IsRequired();
            b.Property(x => x.Preis).HasColumnName(nameof(Produkt.Preis)).IsRequired();
        });

        builder.Entity<Laden>(b =>
        {
            b.ToTable("Ladens"); 
            b.ConfigureByConvention();
            b.HasKey(x => x.Id);
            b.Property(x => x.Name).HasColumnName(nameof(Laden.Name)).IsRequired();
            b.Property(x => x.Preis).HasColumnName(nameof(Laden.Inhaber));
            b.HasMany<Produkt>(p => p.Produkte).WithMany("Laden").UsingEntity(j => j.ToTable("Produkt2Laden"));
        });

    }
}

Hier werden die beiden Entitäten eingerichtet und auch die einseitige N:M-Beziehung von Laden auf Produkte. Entity Framework erzeugt nun im Hintergrund die nötige Zwischentabelle, die hier „Produkt2Laden“ genannt wird. Im optimalen Fall hätte man die Navigationsproperties auf beiden Seiten gesetzt, aber hier geht es ja genau darum, es nur einseitig zu haben.

Der Trick ist hier die Anweisung .HasMany(p => p.Produkte).WithMany("Ladens"). Hier wird es als n:m-Beziehung definiert. Normalerweise wäre Ladens eine Eigenschaft von Produkt. Aber die haben wir ja nicht. Bei WithMany() wird daher kein Lambda, sondern eine Zeichenkette verwendet. Mit Stringliteralen kann man wenigstens etwas faken. Es könnte also irgendwas dort stehen. Aus diesem Grund funktioniert auch nicht Alles komplett. Die Verwendung von .Include(), um die Detailtabelle mitzuladen, wird zum Problem. Es geht nicht unter allen Umständen. So z.B. bei diesem Versuch eines Delete:

public async Task DeleteProduktFromLadenAsync(Guid ladenId, Guid produktId)
{
    Laden entitywithdteails = await this.Where(d => d.Id == ladenId).Include(i=>i.Produkte).FirstOrDefaultAsync();
    Produkt detail = entitywithdteails.Teams.FirstOrDefault(z=>z.Id == orgId);
    if( detail!=null )
    {
        entitywithdteails.Teams.Remove(detail);
    }
}

Dieser Versuch funktioniert nicht. Entity Framework gibt einem eine relativ nichtssagende Fehlermeldung. Auch wenn es nicht nötig ist, möchte EF da scheinbar einmal durch alle drei Relationen durch und wieder zurück. Da die „Rückreferenz“ also die ICollection<Produkt> Läden in Produkt fehlt, geht es per default nicht.
Nebenbei (wenn man also weiß, dass eine N:M-Beziehung über eine Zwischentabelle realisiert wird) ist es auch gar nicht nötig, zunächst auch nur eine der beiden Entitäten ([Laden,Produkt]) zu laden, um an den Beziehungen der beiden zu arbeiten. Umso mehr muss man sich um eine effiziente und zuverlässige Bearbeitung der Detailtabellendaten kümmern.

Coden

Ich bin dabei auf folgende beiden Implementierungen der Add/Remove-Operationen gekommen:

public async Task AddProdukteToLaden(Guid ladenId, Guid produktId)
{
    ErpDbContext context = await GetDbContextAsync();
    // fake element attachen und dann in die Collection rein.
    var prod = new Produkt(produktId, null, null);
    var laden = new Laden (ladenId, null, null);
    context.Attach(laden);
    // zeige EF, was passieren soll
    laden.Produkte.Add(prod);
}
public async Task DeleteProduktFromLadenAsync(Guid ladenId, Guid produktId)
{
    var laden = new Laden(ladenId, string.Empty);
    var produkt = new Produkt(produktId, string.Empty, 0);
    // simuliere Zustand davor
    laden.Produkte.Add(produkt);

    var context = await GetDbContextAsync();
    context.Attach(laden);
    // zeige EF, was du willst
    laden.Produkte.Remove(produkt);
}

Der Trick besteht dabei darin, dass Entity Framework die Hauptentitäten gar nicht unbedingt holen muss. Es ist auch nicht wichtig, was in den Feldern steht. Einzig wichtig ist die Id des Datensatzes. Wenn man so eine Enität an den DB-Kontext per .Attach() anfügt, beginnt das Tracking von Entity Framework ab diesem Zeitpunkt. Werden keine anderen Eigenschaften/Felder verändert, hat es auch keine Updates zur Folge. Ergeo wird in diesem Fall nur das Add/Remove von der Detailkollektion mitgeschnitten und somit in die DB persisitiert.

Wir lernen also: Man braucht nicht die ganzen Entitäten zu laden um an Detailkollektionen Änderungen zu machen. Und: context.SaveChanges() nicht vergessen. In meinem Fall gab es ein Framework drumherum, welches alles in eine UnitOfWork einpackt und somit erfolgreiche Operationen automatisch persisitiert sind. Daher fehlt es bei meinen Beispielen.

Erfolgreiche Softwareentwicklung

In diesem Beitrag versuche ich eine lose Auflistung von Punkten zu bieten, die eine Softwareentwicklung erfolgreich machen. Klar ist: Alles kann nichts muss. Also ist es weder so, dass man alles einsetzen muss, noch ist der Erfolg bei Einsatz garantiert.

Kommen wir also zu meinen Empfehlungen. Vermutlich ist kollidieren sogar einige meiner Empfehlungen. Daher gilt: Nehmt Euch raus, was Euch gefällt und setzt es für Euch richtig um. Denn wie so oft im Leben gibt es mehr als nur schwarz und weiß. Viel Spaß.

  • Einsatz eines Versionskontrollsystems (z.B. GIT)
  • Einsatz von Entwicklungszweigen im VCS (Versionskontrollsystems). Branches.
  • UnitTests: Für einzelne Klassen (Basisbausteine) bis hin zu Komponenten (Fertigbauteile) sollten UnitTests eingesetzt werden und bei CI/CD ausgeführt werden. UnitTests von Anfang an schreiben.
  • Für Komponentenübergreifende Teile sollten Modultests gemacht werden. Testszenarien. GUI-Tests, Replay-Tests und bei Testreleases und sowieso bei Releases ausgeführt werden.
  • Für die Gesamtanwendung sollte eine QA-Abteilung mit Menschen sich dran setzen. Die ganze Zeit und speziell zu Releases.
  • Einrichtung einer CI/CD-Pipeline . UnitTests sollten dort ausgeführt werden, besser: Statische Analysen + Code-Style. Als Ergebnis wird ein Installer/Paket oder ein Deployter Container o.ä. erwartet. Ein Tester kann also gleich ran an den Speck!
  • Release-Versionierung. Es kann für Regressionen wichtig sein, auf einen laufenden früheren Stand zurückzugehen. Also: „War das früher auch schon kaputt, oder ist das neu?“. Daher: Setups reproduzierbar machen (Installer, VMs, Container deployments etc.)
  • Release-Management. Es braucht einen Plan, wie man von Release zu Release kommt und wie ältere gepflegt werden und welche Merkmale „gemerged“ werden.
  • Ticketsystem einsetzen. Es ist unmöglich in einem Wust von Code und Information den Überblick zu behalten. Aufgaben müssen verwaltet werden. Tickets immer mit Commits im Versionskontrollsystem verknüpfen (wo sinnvoll).
  • Logging einsetzen. Erfindet das Rad nicht neu! Nutzt Logging-Frameworks. So kann auch auf externe Server geloggt werden etc.
  • Audit-Log. Je nach Anwendung frühzeitig einführen, denn später anflanschen ist doof. Es gibt immer wieder sicherheitsrelevante Dinge zu loggen -> Audit-Log
  • Baut die Anwendung in Schichten auf. Es hat sich bewährt.
  • ORM ist Pflicht. Die Datenschicht ist oft eine Relationale Datenbank. Vermeidet SQL-Zeug. Überbrückt die OO-ER-Lücke mit einem Object Relational Mapper (ORM) wie z.B. Entity Framework!
  • Scheut Euch nicht, auch mal andere Konzepte auszuprobieren. Sie könnten für das zu lösende Problem eine einfachere, zuverlässigere Lösung parat haben. Genannt sei das Aktor-Modell oder Reactive oder Prolog-artige Horn-Klauseln.
  • Baut Internationalisierung (i18n) von Anfang an ein. Das schärft gleich den Sinn, wann etwas lokalisiert dargestellt wird, und wann eine Darstellung kulturinvariant sein soll (bei Persistenz). Außerdem: Später hinzufügen ist wieder mal schlecht und teuer.
  • Baut Barrierefreiheit (accessiblity, a11y) von Beginn an ein. Es ist inzwischen in manchen Ländern oder Bereichen (öffentliche Hand) Pflicht. Aber: Großes Thema, nicht einfach. Screenreader sollten aber an den Text kommen können.
  • Setzt immer Unicode ein. Geht davon aus, dass die Anwender alle gültigen Zeichen der Welt einsetzen wollen und werden. Kodiert Dateien mit UTF-8-BOM.
  • Lernt bei Developer Falsehoods und dem gigantischen Git-Repo über Falsehoods, was so die typischen Fehlannahmen sind und vermeidet sie. Schon gewusst: Vor+ Nachname sind eine Besonderheit, die es hier gibt.
  • Bedenkt Sicherheit im Sinne von Security und setzt Verschlüsselung ein. Nutzt aber immer Bibliotheken und erfindet nichts selbst.
  • Paarprogrammierung. Setzt das XP-Merkmal der Paar-Programmierung ein. Ein Junior kann von einem Senior so viel lernen und Umgekehrt. Oder Wissen aus verschiedenen Programmbereichen verteilen. Vorteil: Es gibt nicht mehr einzelne Koryphäen, da sich Wissen dupliziert. Man lernt Programmiertechniken und Prozesse und die Entwickler sind konzentrierter dabei und machen weniger Fehler, was den „doppelten Aufwand“ mehr als Wett macht.
  • Nutze TDD – Test driven develoment. Nicht überall aber bei Kernkomponenten/Klassen empfohlen. Die dabei entstehenden UnitTests können gleich bleiben und in der CI-Pipeline verwendet werden.
  • Coding-Standard. Entwickelt einen Formatierungsstandard und forciert ihn mit Programmen wie StyleCop.
  • Code-Reviews. Macht z.b. alle 14 Tage ein öffentliches Review. Das ist ein unglaublich gutes Werkzeug, um Fehler zu finden und einander Einblick und Tricks zu vermitteln.
  • Check-In mit Pull-Requets und 4-Augen-Prinzip. Nutzt die Mechanismen, die moderne Entwicklungsplattformen bieten. Bei Git gibt es einen zweistufigen Commit mit Code-Review. Nutzt das und lasst einen Check-In immer von einer anderen Person reviewen. Es hilft immens, Fehler von Beginn an zu vermeiden.
  • Refaktorisieren. Mut zur Refaktorisierung. In der Regel kommt was besseres dabei raus. Schiefe Balken müssen gerade gerichtet werden. Nutzt Tools dazu.
  • Kommentiert, aber auch nicht zu viel. Dokumentation veraltet schnell, Kommentare veralten auch. Daher Pflegt zumindest diese. Keine Kommentare ist auch falsch. Mittelweg! Bewährt hat sich, öffentliche Methoden zu kommentieren mit (ohoh) XML-Doc und die Klasse an sich. Dies gefällt mir insbesondere bei fremdem Code, wenn wieder „die nächste Klasse“ auftaucht, und man wieder sich fragt : „warum ist diese Klasse jetzt nötig, was verdammt soll ihre Aufgabe jetzt genau sein?“. Wer mir diese Frage gleich oben beantwortet (und die sollte recht konstant bleiben), der hat bei mir einen Stein im Brett! Sparam im Quellcode zu kommentieren ist auch keine gute Idee. Ich vergesse recht schnell, welche kranken und doch genialen Ideen ich da hatte.
  • Nutzt schlaue Tools. Tools, die Euch das Leben einfacher machen und z.B. Code überprüfen, generieren oder automatisch umstrukturieren. Genannt sei hier z.B. Re-Sharper. Viele haben Angst vor der Automatik, aber sie ist deterministisch und wenn man es einmal gelernt hat, ist sie ein Segen. Denn sie denkt meist sogar an mehr, als man selbst. Dazu gehören auch Analysetools, wie z.B. der Nachfolger von FxCop oder LINTer. Sie analysieren Code auf typische Fehler und weisen Entwickler darauf hin.
  • Automatisiert, wo es geht. Das ist DevOps. Alle dummen, manuellen Schritte sollten wenn möglich automatisch getriggert und ausgeführt werden.

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: