code it

Martins Tech Blog

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.

Global Assembly Cache reloaded

Das .NET Framework 4 ist nun schon ein paar Tage alt und abgesehen von tollen neuen Features bringt es auch einige gewöhnungsbedüftige Sachverhalte mit sich. Einer davon ist die Änderung im Global Assembly Cache.

Wer bisher die Fusion-Ansicht des Global Assembly Cache verwendet hat und per Drag & Drop Assemblies installiert hat, wird bemerken, dass alle neuen Assemblies sich nicht mehr installieren lassen, obwohl keine Fehlermeldung erscheint. Installiert man die Assemblies hingegen per gacutil, so wird man feststellen, dass zwar eine Erfolgsmeldung kommt, aber unter C:\Windows\assembly keine Veränderung geschieht.

Woran liegt das? Mit dem .NET Framework 4 gab es einige Änderungen im Konzept des GAC. Hier ein kurzer Auszug aus der MSDN:

In .NET Framework 4.0, the GAC went through a few changes. The concept of placing assemblies into a global directory began in CLR v1.1. In case of .NET Framework 1.1 (which had CLR v1.1) and .NET Framework 2.0 (which had CLR 2.0), the GAC was split into two, one for each CLR. This avoided the leaking of assemblies across CLR versions. For example, if both .NET 1.1 and .NET 2.0 shared the same GAC, then a .NET 1.1 application, loading an assembly from this shared GAC, could get .NET 2.0 assemblies, thereby breaking the .NET 1.1 application.

The CLR version used for both .NET Framework 2.0 and .NET Framework 3.5 is CLR 2.0. As a result of this, there was no need in the previous two framework releases to split the GAC. The problem of breaking older (in this case, .NET 2.0) applications resurfaces in Net Framework 4.0 at which point CLR 4.0 released. Hence, to avoid interference issues between CLR 2.0 and CLR 4.0, the GAC is now split into private GACs for each runtime.

Zusammengefasst bedeutet das, dass der GAC nicht mehr für alle Assemblies C:\Windows\assembly ist, sondern dass dort alle Pre-Framework-4-Assemblies liegen. Alle neuen Assemblies liegen im Assembly-Ordner des .NET-Frameworks (also z.B. C:\Windows\Microsoft.NET\assembly).

 

Für mich erklärt der MSDN-Auszug nun nicht ganz, warum hier ein anderer Ordner notwendig war, der sich auch noch anders verhält als der bisherige (s. Bild) - irgendwie hatte man sich ja schon an die Fusion-Ansicht gewöhnt. Für mich bleiben bei der aktuellen Implementierung viele Fragen offen:

  • Hätte man nicht unter C:\Windows\assembly noch eine Ordnerebene einführen können, die die Frameworkversion widerspiegelt?
  • Weshalb hat der neue GAC keine Fusion-Ansicht, in der so wichtige Informationen wie der Public-Key-Token und die Culture auf einen Blick sichtbar sind?
  • Weshalb kann die Fusion-Ansicht im alten GAC per Drag & Drop nicht selbstständig in die richtigen Ordner installieren?
  • ....

Glücklich bin ich mit der jetzigen Lösung nicht, aber ich werde mich - wie viele andere - damit auch arrangieren. Und vielleicht kommt ja noch ein Update oder ServicePack, das alles wieder schön macht. 

Einladung zum Treffen der .NET Usergroup Dresden am 25.08.2010

Das nächste Treffen der .NET Usergroup Dresden findet am 25.08.2010 statt. Austragungsort ist dieses Mal die Communardo Software GmbH.

Wie bereits im Juli-Treffen angekündigt werden dabei folgende Themen eine Rolle spielen:

Branching/Merging Strategien mit dem TFS (Robert Mühsig)
Nachdem wir bei der letzten Usergroup den Themenkomplex Buildmanagement vertieft haben, setzt Robert seinen Vortrag zum Thema TFS fort. Dieses Mal geht es um Branching- und Merging-Strategien.

Einführung in die Entwicklung für Windows Phone 7 (Martin Hey)
Windows Phone 7 wird dieses Jahr gelauncht. Der Vortrag soll eine Einführung in die Möglichkeiten und Besonderheiten der Entwicklung für Windows Phone 7 geben.

Das Treffen beginnt wie immer um 18:00 Uhr. Wer daran interessiert ist zu kommen, den bitten wir sich in einer der Teilnehmerlisten (Xing oder Doodle) einzutragen. Nach dem Treffen ist jeder gern eingeladen, beim Stammtisch an einem Plausch in lockerer Atmosphäre über .NET oder alles andere teilzunehmen.

Nachlese zum Treffen der .NET Usergroup am 21.07.2010

Gestern Abend trafen sich wieder begeisterte .NET Anhänger zur .NET Usergroup. Erfreulich war, dass wir ganz viele neue Gesichter dabei hatten, was mir zeigt, dass wir auf dem richtigen Weg sind und was uns dazu veranlasst hat, zu Beginn noch eine kurze Vorstellungsrunde zu setzen.
 
Direkt danach ging es schon tief ins Fachliche und ich habe anhand eines Beispiels gezeigt, wie man dynamische LINQ-Queries mit Hilfe einer geschickten Kombination der Klassen aus dem Namespace System.Linq.Expressions generieren kann. Hier hat sich auch gut gezeigt, wie die Community funktioniert, denn eines meiner Beispiele funktionierte nicht auf Anhieb (das Risiko des Live-Codings) und die hilfreichen Tipps von den Zuschauern haben mich auf die richtige Spur gebracht. Dass hier Interesse besteht, hat auch die anschließende kurze Diskussion "Typsicherheit vs. Dynamik", welche sich aus dem Thema entwickelt hat, gezeigt. Für das Beispielprojekt gibt es auch einen Downloadlink.
Nach einer kurzen Pause ging es mit Robert dann in die Tiefen automatischer Builds mit Hilfe von Team Foundation Server 2008 und 2010 und MSBuild-Skripten. Zwar hatte jeder der Anwesenden sich schon einmal mit dem Thema beschäftigt, aber in den Tiefen von MSBuild waren nur sehr wenige bisher unterwegs. Allen Teilnehmern, die sich etwas eingehender mit dem Thema MSBuild beschäftigen wollen, sei die MSBuild Serie von Thorsten Hans ans Herz gelegt, die Robert ja kurz erwähnt hat.
 
Im Anschluss an die Usergroup konnte man dann bei einem kühlen Bier in der Terrasse am Bischofsplatz noch fachsimpeln und den Abend ausklingen lassen.

Exam 71-573: Microsoft SharePoint 2010 Application Development

Mitte Mai bekam ich eine Mail von Microsoft, in der ich gefragt wurde ob ich nicht an den Beta-Prüfungen zu SharePoint 2010 teilnehmen möchte. Zu jeder der vier neuen Prüfungen gab es einen Promo-Code. Da es zeitlich gerade nicht ganz so rosig aussah, entschied ich mich zumindest eine der Prüfungen anzugehen und die MCTS in WSS 3.0 Application Development und MOSS 2007 Application Development auf SharePoint 2010 anzuheben.
Das spannende an Beta-Prüfungen ist: Man erfährt nicht sofort, ob man bestanden hat. Man ist ja gemeinsam mit vielen anderen das "Versuchskaninchen" für die neuen Fragen. Oft dauert die Auswertung mehrere Wochen oder bis zum Release der eigentlichen Prüfung. Heute morgen nun war es soweit: Eine neue Mail von Microsoft berichtet mir davon, dass ich das Exam bestanden habe und nun MCTS für SharePoint 2010 Application Development bin.
Ich freue mich darüber und wenn ich wieder eine Einladung zu einer Beta-Prüfung bekomm, werd ich das Angebot sicher wieder in Anspruch nehmen.

Wie verhindere ich einen doppelten Submit?

Die Verarbeitung von Postbacks in einem ASP.NET Formular benötigt gelegentlich etwas mehr Zeit. Beispiele dafür sind der Upload von Dateien oder die zeitintensive Verarbeitung der Anfrage in Backend-Systemen. Im Frontend bekommt der Benutzer davon meist nicht viel mit - abgesehen von einem Ladebalken im Browser. Da hier nur wenig Rückmeldung ist, bietet es sich an, dem Benutzer irgendeine Art von Feedback zu geben - sei es mit dem Einblenden einer Nachricht à la "Gedulden Sie sich, Ihre Anfrage wird verarbeitet.", oder dem simplen Deaktivieren des Buttons, der den Postback auslöst, damit mehrfache Submits verhindert werden, die den Server lahmlegen könnten. Den zweiten Ansatz verfolgt dieser Blogpost. 

Problem daran: sämtliche Aktionen, die im Codebehind der Seite passieren könnten werden erst aktiv, wenn der Client eine Response bekommt - heißt: ein C# SubmitButton.Enabled = false käme in diesem Fall zu spät. Einzig sinnvolle Lösung ist daher, sich an das JavaScript anzuhängen, dass den Postback ausführt. Ein kurzer Blick in die MSDN bringt die passende Methode zum Vorschein - RegisterOnSubmitStatement in der Klasse ClientScriptManager.

// register JS to disable submitbutton
if (!Page.ClientScript.IsOnSubmitStatementRegistered("disableSubmitScript"))
{
    Page.ClientScript.RegisterOnSubmitStatement(Page.GetType(), "disableSubmitScript", "DisableSubmitButtonOnSubmit('" + saveTicketButton.ClientID + "');");
}

Als Parameter wird der Funktionsaufruf übergeben, der den Submit-Button deaktiviert. Handelt es sich um einen Button, so kann man ihn auf disabled setzen, in meinem Fall liegt ein LinkButton vor. Um diesen zu deaktivieren muss das href-Attribut entfernt werden. Meine Methode DisableButtonOnSubmit, die ich der Methode als OnSubmitStatement genannt habe, sieht also wie folgt aus:

// JS function to disable a linkbutton
function DisableSubmitButtonOnSubmit(controlId) {
    var control = document.getElementById(controlId)
    if (control != null) {
        // disable linkbutton
        control.attributes.removeNamedItem("href");
        // disable button
        //control.disabled = true;
    }
}

Um das Aktivieren muss man sich nun nicht mehr kümmern, da das Attribut ja automatisch wieder gesetzt ist, wenn der Postback durchgelaufen ist und die Response vom Server verarbeitet wurde. Stimmt soweit, wenn da nicht andere Skripte wären, die auch noch ausgeführt werden. Liegen im Formular nämlich Validatoren (wie RequiredFieldValidator oder RegExValidator), die die Anfrage clientseitig validieren. Dann wird der Klick durchgeführt, das eben erstellte Skript deaktiviert den Button und im Anschluss daran setzen die Validatoren das Formular auf ungültig und der Anwender kann nicht mehr auf den Button klicken. Schön wäre jetzt, wenn das Skript nun noch erkennen würde, ob Validatoren greifen und dann den Button nicht deaktivieren würde. Dazu muss das JavaScript nun noch etwas angepasst werden.

// JS functions to disable submit linkbutton
function DisableSubmitButtonOnSubmit(submitButtonControlId) {
    if (typeof (ValidatorOnSubmit) == "function" && ValidatorOnSubmit() == false) {
        return false;
    }
    else {
        DisableControl(submitButtonControlId);
        return true;
    }
}

function DisableControl(controlId) {
    var control = document.getElementById(controlId)
    if (control != null) {
        // disable linkbutton
        control.attributes.removeNamedItem("href");
    }
}

Automatisierung des Windows 7 Phone Emulators

Eine Windows Phone 7 Anwendung erstellen ist so einfach: Man öffnet Visual Studio und erzeugt ein neues Projekt auf Basis des Windows Phone Templates; entwickelt die Anwendung und deployt sie auf den integrierten Phone-Emulator. Alles gut und schön, bis .... ja bis der Kunde sagt, er möchte sich das Look & Feel auch mal ansehen und die Anwendung ausprobieren.

Nun kann man dem Kunden ja schlecht sagen: "Hier sind die Sourcen. Installieren Sie das Windows Phone SDK. Öffnen Sie die Sourcen dann in Visual Studio (Express). Drücken Sie Compile. Und wenn Sie alles richtig gemacht haben, tut Visual Studio und der Emulator öffnet sich mit der Anwendung." Spätestens bei Schritt 4 dieser Anleitung steigt er aus und wird uns was flüstern.

Also muss eine für ihn einfachere Lösung her. Einen sehr guten Post zu diesem Thema hat Justin Angel vor kurzem verfasst, den ich hier aufgreifen möchte. Dabei wird eine Konsolenapplikation erstellt, die beim Kunden Windows Phone 7 emulieren und die Anwendung installieren und starten kann.

Zunächst erstellt man eine Konsolen-Applikation und referenziert die Datei Microsoft.SmartDevice.Connectivity.dll, welche sich im Ordner "C:\Program Files\Common Files\microsoft shared\Phone Tools\CoreCon\10.0\Bin" befindet und schon kann die Implementierung beginnen. Die durchzuführenden Schritte sind:

  • Windows Phone 7 CoreCon SDK Instanz holen
  • Emulator instanziieren
  • zum Emulator verbinden
  • vorherige Versionen deinstallieren
  • XAP-Datei installieren
  • Anwendung starten

In Quellcodezeilen ausgedrückt sieht das in etwa wie folgt aus:

string appGuid = "{8bf5502f-8d01-4b06-abd8-d3195e838cfd}";
string appXAP = "MyApplication.xap";

// Get CoreCon WP7 SDK
DatastoreManager dsmgrObj = new DatastoreManager(1033);
Platform WP7SDK = dsmgrObj.GetPlatforms().Single(p => p.Name == "Windows Phone 7");

// Get Emulator / Device
bool useEmulator = true;
Device WP7Device = null;
if (useEmulator)
   WP7Device = WP7SDK.GetDevices().Single(d => d.Name == "Windows Phone 7 Emulator");
else
   WP7Device = WP7SDK.GetDevices().Single(d => d.Name == "Windows Phone 7 Device");

// Connect to WP7 Emulator / Device
Console.WriteLine("Connecting to Windows Phone 7 Emulator/Device...");
WP7Device.Connect();
Console.WriteLine("Windows Phone 7 Emulator/Device Connected...");
Guid appID = new Guid(appGuid);
RemoteApplication app;
if (WP7Device.IsApplicationInstalled(appID))
{
   Console.WriteLine("Uninstalling sample XAP to Windows Phone 7 Emulator/Device...");

   app = WP7Device.GetApplication(appID);
   app.Uninstall();

   Console.WriteLine("Sample XAP Uninstalled from Windows Phone 7 Emulator/Device...");
}

// Install XAP
Console.WriteLine("Installing sample XAP to Windows Phone 7 Emulator/Device...");

string curdir = ApplicationPath();

app = WP7Device.InstallApplication(
   appID,
   appID,
   "NormalApp",
   curdir + @"\ApplicationIcon.png",
   curdir + "\\" + appXAP);
Console.WriteLine("Sample XAP installed to Windows Phone 7 Emulator...");

// Launch Application
Console.WriteLine("Launching sample app on Windows Phone 7 Emulator...");
app.Launch();
Console.WriteLine("Launched sample app on Windows Phone 7 Emulator...");

Wichtig ist der Name der XAP-Datei, damit die Anwendung installiert sowie die Guid der Anwendung (zu ermitteln aus der WMApplication.xaml), damit die Anwendung vom Gerät auch wieder deinstalliert werden kann.