code it

Martins Tech Blog

Build Messages in CodeActivities in Teambuild 2010

Wer in TFS 2008 eigene Build-Tasks geschrieben hat und dabei von der abstrakten Basisklasse Task abgeleitet hat, wird wissen,dass es recht einfach war, Meldungen ins Log zu schreiben. Die Klasse stellte ein Objekt Log zur Verfügung, das über die passenden Methoden verfügte.

In TFS 2010 ist die Workflow-Engine Grundlage des Teambuilds. Dieser ist auch weiterhin erweiterbar - nur wird nun nicht mehr von der Klasse Task, sondern von der Klasse CodeActivity geerbt. Nur leider bietet diese Klasse eben kein solches Log-Objekt mehr.

Abhilfe schafft hier der Microsoft.TeamFoundation.Build.Workflow.Activities-Namespace. Ist dieser über eine using-Direktive eingebunden, so stehen auf dem context-Objekt drei Extension-Methods zur Verfügung, die das Logging übernehmen können.

using System.Activities;
using Microsoft.TeamFoundation.Build.Workflow.Activities;

namespace CustomActivityLibrary
{
    public sealed class CodeActivity1 : CodeActivity
    {
        protected override void Execute(CodeActivityContext context)
        {
            context.TrackBuildMessage("this is a message");
            context.TrackBuildWarning("this is a warning");
            context.TrackBuildError("this is an error");
        }
    }
}

Erstellung einer StartRemoteProcess-Build-Activity

Die neuen Build-Workflows für den Team Foundation Server sind ein cooles Feature. Und wenn man Standard-Projekttypen verwendet funktioniert das auch alles super, da in den meisten Fällen Copy&Paste-Deployment ausreichend ist.

In meinem Anwendungsfall war es notwendig, am Ende des Builds eine Activity auszuführen, die auf einem Remote-Server (z.B. Integrationssystem) einen Prozess startet. Eine solche Activity habe ich im Standard nicht gefunden. Daher hab ich selbst eine geschrieben.

Eine eigene Activity zu erstellen ist gar nicht so schwer. Einen guten Einstieg geben die Posts von Jim Lamb und Ewald Hofman. Bei der Entwicklung der Activity hat sich die Projektaufteilung von Ewald als sehr praktisch erwiesen. Nur so war es mir möglich, die selbst erstellte Activity dem Workflow dann auch hinzuzufügen.

Um einen Prozess zu starten, sind folgende Informationen notwendig:

  • Name oder IP-Adresse des Remote-Servers
  • auszuführendes Command
  • Credentials (abweichend vom TFS-Service-Account)

Daher erhält die Activity in Summe fünf Input-Argumente: Command, RemoteMachine, Domain, UserName und Password. Damit sieht der Rumpf der Actitvity wie folgt aus:

 

[BuildActivity(HostEnvironmentOption.All)]
public sealed class StartProcessOnRemoteMachine : CodeActivity
{
    [RequiredArgument]
    public InArgument<string> Command
    {
        get;
        set;
    }

    [RequiredArgument]
    public InArgument<string> RemoteMachine
    {
        get;
        set;
    }

    [RequiredArgument]
    public InArgument<string> Username
    {
        get;
        set;
    }

    [RequiredArgument]
    public InArgument<string> Password
    {
        get;
        set;
    }

    [RequiredArgument]
    public InArgument<string> Domain
    {
        get;
        set;
    }
}

Bei der Implementierung sollte man sich überlegen, so sensible Daten wie Credentials ggf. anders abzubilden als per Input-Parameter - für dieses Beispiel soll diese Lösung aber ausreichend sein, damit es nicht zu komplex wird. Die eigentliche Arbeit übernimmt dann die im folgenden dargestellte Methode ExecuteProcessOnRemoteMachine, die per WMI einen Prozess auf einem anderen Rechner startet. Dazu ist es notwendig, die Assembly System.Management zu referenzieren.

private static void ExecuteProcessOnRemoteMachine(string remoteMachine, string username, string password, string domain, string commandLine)
{
    ConnectionOptions connectionOptions = new ConnectionOptions();
    connectionOptions.Authority = "ntlmdomain:" + domain;
    connectionOptions.Username = username;
    connectionOptions.Password = password;
    connectionOptions.Authentication = AuthenticationLevel.Default;
    connectionOptions.Impersonation = ImpersonationLevel.Impersonate;
    connectionOptions.EnablePrivileges = true;

    ManagementScope managementScope = new ManagementScope(string.Format(@"\\{0}\ROOT\CIMV2", remoteMachine), connectionOptions);
    managementScope.Connect();

    ManagementPath managementPath = new ManagementPath("Win32_Process");
    ManagementClass processClass = new ManagementClass(managementScope, new ManagementPath("Win32_Process"), new ObjectGetOptions());
    ManagementBaseObject inParams = processClass.GetMethodParameters("Create");
    inParams["CommandLine"] = commandLine;

    ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null);
}

Der Rest ist trivial: In der Methode Execute werden die Input-Parameter entgegengenommen und an die Methode ExecuteProcessOnRemoteMachine übergeben.

protected override void Execute(CodeActivityContext context)
{
    string startProcessCommand = context.GetValue(this.Command);
    string remoteMachine = context.GetValue(this.RemoteMachine);
    string username = context.GetValue(this.Username);
    string password = context.GetValue(this.Password);
    string domain = context.GetValue(this.Domain);


    ExecuteProcessOnRemoteMachine(remoteMachine, username, password, domain, startProcessCommand);
}

Die Activity wird nun in den Workflow eingebunden und die notwendigen Daten bereitgestellt.

Am Ende des Builds werden nun die Dateien aus dem DropFolder genommen und mittels der Standard-Activity CopyDirectory auf den Integrationsserver kopiert. Die neu erstellte Activity führt dann die Installation durch.

Zugriff auf erweiterte Eigenschaften einer Workflowaufgabe

Bei der Entwicklung komplexer Workflows für den SharePoint spielen auch Workflowaufgaben für die Benutzerinteraktion während der Ausführung des Workflows eine nicht unbedeutende Rolle. Eine solche Aufgabe hat nicht nur die Standardeigenschaften wie einen Titel oder eine Beschreibung sondern auch erweiterte Eigenschaften. Das praktische daran ist, dass diese kein eigenes Feld in der Liste benötigen sondern die Daten in den bereits vorhandenen Feldern gespeichert werden. Und wenn man weiß, wie es geht, ist der Zugriff auf diese Daten auch sehr einfach.

1.) bei der Erstellung
Während der Erstellung der Aufgaben verwendet man in der Regel den Typ WssTaskActivity. Dieser verfügt bereits über eine Eigenschaft ExtendedProperties, die vom Typ Hashtable ist und in die man seine gewünschten Daten schreiben kann.

createTask.TaskId = Guid.NewGuid();
createTask.TaskProperties.Title = "Bitte genehmigen";
createTask.TaskProperties.ExtendedProperties["AdditionalText"] 
    = "Bitte persönlich bearbeiten!";

2.) über den Item der TaskList
Möchte man nun später noch einmal darauf zugreifen, sieht man sich vor die Aufgaben gestellt, die Daten wieder aus dem SPListItem der TaskList zu lesen und nach der Änderung auch wieder zu schreiben. Sieht man sich das SPListItem genauer an, das die Aufgabe repräsentiert, so erkennt man, dass es ein Feld ExtendedProperties enthält, in dem die Daten auch enthalten sind - allerdings sind hier einfach die XML-Attribute die die Eigenschaften repräsentieren angegeben:

"AdditionalText='Bitte persönlich bearbeiten!' Department='Einkauf'"

Die große Frage ist jetzt, wie man einfach auf diese Daten zugreift, diese verändert und wieder abspeichert. Die Hashtable die bei der Erstellung wäre hierfür ein adäquates Objekt, das hier aber wie es zunächst scheint nicht vorhanden ist. Aber man muss hier nicht verzagen - auch dafür gibt es eine bereits vorhandene Lösung, auf die man aber erst kommen muss. Der Typ SPWorkflowTask aus dem NameSpace Microsoft.SharePoint.Workflow verfügt über 2 interessante statische Methoden: GetExtendedPropertiesAsHashtable hat als Rückgabewert genau die eben noch vermisste Hashtable und AlterTask verfügt über die Möglichkeiten, den Item mit den geänderten Daten auch wieder zu aktualisieren.

// get the extended properties hashtable
Hashtable taskItemExtProps = SPWorkflowTask
    .GetExtendedPropertiesAsHashtable(this.TaskListItem);
// write new values to the hashtable
taskItemExtProps["AdditionalText"] = "Mein Kommentar: genehmigt!";
// update the task item with new values
SPWorkflowTask.AlterTask(this.TaskListItem, taskItemExtProps, true);

Error loading workflow: Runtime capabilities are not available with this type

Als ich nach mehreren Tagen mal wieder einen Sharepoint-Workflow in die Design-Ansicht laden wollte, bekam ich diese Meldung angezeigt.

Der Text, der wie ein Link aussieht, half mir auch nicht wesentlich weiter, denn bei Klick passierte nichts. Auch erneutes Abrufen aus der Quellcodeverwaltung und eine Neukompilierung des Projekts brachte keine Abhilfe.

Schließlich brachte mich eine intensive Suche zu einem Blogpost, in dem genau das Problem beschrieben stand. Die Lösung: Die komplette Solution muss einmal fehlerfrei kompiliert werden, dann klappt auch wieder die Designansicht....