Visual Studio Projekt Files für Release Builds erweitern

13. Oktober 2009

Ich bin momentan in den Vorbereitungen um eine kleine Library auf CodePlex zu publizieren. Ich will auf der Projektseite nebst dem Source Code, den man ja einfach via Subversion Client hochladen kann, auch binäre Releases zum Download anbieten.

So ein Release von meinem kleinen Projekt besteht aber immerhin aus einigen Files. Nebst dem eigentlichen Assembly wären da noch Bilddateien, Stylesheets, JavaScripts und natürlich auch die notwendigen externen Libraries, die benötigt werden. Nun wäre es doch sehr mühsam, für jeden Release alle diese Dateien immer wieder einheitlich und genau gleich zusammenzukopieren. Zum Glück lässt sich dies relativ einfach mit einer kleinen Erweiterung des Visual Studio Projektfiles erreichen.

Die Visual Studio Projektdatei ist sogar schon dafür vorbereitet. Am Ende jeder .csproj Datei (wahrscheinlich in ähnlicher Form auch bei .vbproj Files) findet man folgenden Kommentar:

<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
 Other similar extension points exist, see Microsoft.Common.targets.
 <Target Name="BeforeBuild">
 </Target>
 <Target Name="AfterBuild">
 </Target>
 -->

Microsoft hat also zwei definierte Targets vorgesehen, welche für eigene Erweiterungen genutzt werden können. Wenn nun der Build beendet wird, wird das Target “AfterBuild” ausgeführt. Hier kann man also durch eigene Tasks wiederkehrende Aufgaben automatisieren lassen. Das Zusammenkopieren meiner Dateien in einen Release Ordner und Anlegen einer Zip Datei wird so zum Kinderspiel:

<PropertyGroup>
  <MSBuildCommunityTasksPath>$(MSBuildProjectDirectory)\..\..\lib\MSBuild.Community.Tasks</MSBuildCommunityTasksPath>
  <ReleasePath>$(MSBuildProjectDirectory)\..\..\Release</ReleasePath>
</PropertyGroup>

<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" />

<Target Name="AfterBuild" DependsOnTargets="ClearRelease" Condition="'$(Configuration)' == 'Release'">
  <!-- Create an item collection of assemblies and other artifacts produced by the build -->
  <ItemGroup>
    <EtcDll Include="$(MSBuildProjectDirectory)\bin\Release\etcetera.Mvc.dll" />
    <MvcContrib Include="$(MSBuildProjectDirectory)\bin\Release\mvccontrib.dll" />
    <Css Include="$(MSBuildProjectDirectory)\UI\css\**\*.*" />
    <Images Include="$(MSBuildProjectDirectory)\UI\images\**\*.*" />
    <Scripts Include="$(MSBuildProjectDirectory)\UI\scripts\**\*.*" />
    <ZipFiles Include="$(MSBuildProjectDirectory)\Release\**\*.*" />
  </ItemGroup>

  <Copy SourceFiles="@(EtcDll)" DestinationFolder="$(ReleasePath)" />
  <Copy SourceFiles="@(MvcContrib)" DestinationFolder="$(ReleasePath)\Dependencies" />
  <Copy SourceFiles="@(Css)" DestinationFolder="$(ReleasePath)\css" />
  <Copy SourceFiles="@(Images)" DestinationFolder="$(ReleasePath)\images\etcetera" />
  <Copy SourceFiles="@(Scripts)" DestinationFolder="$(ReleasePath)\scripts" />

  <!-- Create a release zip file using the MSBuild Community tasks -->
  <ItemGroup>
    <ZipFiles Include="$(ReleasePath)\**\*.*" />
  </ItemGroup>
  <Zip Files="@(ZipFiles)" ZipFileName="$(ReleasePath)\etcetera.Mvc.zip" WorkingDirectory="$(ReleasePath)" />
  <Message Text="Copied all etcetera.Mvc files to folder $(MSBuildProjectDirectory)\Release" Importance="high" />
</Target>

<Target Name="ClearRelease">
  <ItemGroup>
    <OldReleaseFiles Include="$(ReleasePath)\**\*.*" />
  </ItemGroup>
  <Message Text="Clearing files in folder $(ReleasePath)" Importance="high" />
  <Delete Files="@(OldReleaseFiles)" />
</Target>

Das Zusammenstellen des Releases soll aber nur stattfinden, wenn auch die “Release” Konfiguration ausgewählt ist. Dafür kann das Target eine Condition auswerten. Zuvor sollen aber die Files des letzten Builds gelöscht werden, deshalb wird das “AfterBuild” Target vom “ClearRelease” Target abhängig gemacht. Somit wird dieses zuerst ausgeführt.

Zusätzlich zu den von Microsoft bereitgestellten Tasks (wie Copy, Delete etc.) lassen sich auch eigene oder Tasks von Dritten verwenden. Mein Beispiel verwendet den Zip Task der äusserst praktischen MSBuild Community Tasks.

Wenn ein Text im Output Fenster von Visual Studio ausgegeben werden soll, muss das “Importance” Attribut des Message Tasks auf “high” gesetzt werden.

Wenn die Projekt Datei derart angepasst wird, kann es sein, dass Visual Studio beim Laden des Projekts warnt, dass die Projektdatei erweitert wurde.

Die WCF mit einem eigenen Nachrichten Logger erweitern

7. Oktober 2009

Die WCF bietet einigen Diagnose Komfort an, um Problemen bei der Übertragung von Nachrichten auf die Spur zu kommen. Dies insbesondere wenn nicht nur WCF Dienste oder Clients beteiligt sind.
Das Message Logging kann benutzt werden, um den Inhalt von oder an Clients und Services aufzuzeichnen. Das Logging kann konfiguriert werden, um Nachrichten auf dem Service Level, dem Transport Level, oder fehlerhaft Nachrichten auszuzeichnen.
Das Logging ist per Default ausgeschaltet und kann so über die WCF Konfiguration eingeschaltet werden:

&lt;system.serviceModel&gt;
...
    &lt;diagnostics&gt;
        &lt;messageLogging logEntireMessage=&quot;true&quot;
                        logMalformedMessages=&quot;true&quot;
                        logMessagesAtServiceLevel=&quot;true&quot;
                        logMessagesAtTransportLevel=&quot;false&quot;
                        maxMessagesToLog=&quot;1000&quot;&gt;
            &lt;filters&gt;
                &lt;clear/&gt;
                &lt;add&gt;/*[local-name()='Envelope']/*[local-name()='Header']/*[local-name()='Action'][text()='http://tempuri.org/Operation']&lt;/add&gt;
            &lt;/filters&gt;
        &lt;/messageLogging&gt;
    &lt;/diagnostics&gt;
&lt;/system.serviceModel&gt;

&lt;system.diagnostics&gt;
    &lt;sources&gt;
        &lt;source name=&quot;System.ServiceModel.MessageLogging&quot;&gt;
            &lt;listeners&gt;
                &lt;add name=&quot;messages&quot;
                     type=&quot;System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089&quot;
                     initializeData=&quot;messages.svclog&quot;/&gt;
            &lt;/listeners&gt;
        &lt;/source&gt;
    &lt;/sources&gt;
    &lt;trace autoflush=&quot;true&quot;/&gt;
&lt;/system.diagnostics&gt;

Dieses Beispiel zeichnet alle Nachrichten auf, welche über die Service Methode “Operation” ein- oder ausgehen. Diese Einschränkung kann durch den Filter als XPath Ausdruck definiert werden.
Das Log wird in einer Datei gespeichert. Diese kann dann recht komfortabel mit dem Service Trace Viewer des SDKs ausgewertet werden.

Wenn nun aber die eigentliche Applikation selber ein Log führt, kann es bei der Fehlersuche recht mühsam sein, die beiden Logs zeitlich miteinander zu korrelieren. Viel praktischer wäre es doch, die WCF Nachrichten und das Applikationslog in einem einzigen Log zu haben.
Ich verwende meistens das Logging Framework log4net.
Die umfangreichen Erweiterungsmöglichkeiten der WCF machen es nun recht einfach, die WCF Nachrichten auch in das bereits vorhandene und konfigurierte log4net Log zu schreiben. Ob das Log in einer Datei oder einer Datenbank geführt wird, ist dabei völlig dem Logging Framework übrlassen.

Die sogenannten “Custom Behaviors” ermöglichen es, Code innerhalb der WCF Runtime und der Message Pipeline ausführen zu lassen. Diese Behaviors können via Code oder Konfiguration hinzugefügt werden. Ein solches Behavior ist also ideal für einen Logging Mechanismus.

Um ein Custom Behavior zu implementieren, sind drei Schritte notwendig:

  1. Erstelle ein Klasse, welche ein Inspector, Selector, Formatter oder Invoker Interface implementiert. Diese Klasse definiert dann meistens auch das eigentliche Verhalten des Behaviors. Um nun alle eingehenden Nachrichten an einen Service zu loggen, kann das Interface IDispatchBehaviour implementiert werden und in dessen Methode AfterReceiveRequest die Nachricht geloggt werden. Wenn auch die vom Service oder Client gesendeten Antworten mitgeschnitten werden sollen, kann zusätzlich das Interface IClientMessageInspector implementiert werden, und die ausgehende Nachricht in dessen AfterReceiveReply Methode geloggt werden.
    public class MessageInspector : IClientMessageInspector, IDispatchMessageInspector
    {
        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            Log.InfoFormat(GetType(), &quot;MessageInspector::BeforeSendRequest(): Sending the following request{0}{1}&quot;, Environment.NewLine, request);
            return null;
        }
    
        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            Log.InfoFormat(GetType(), &quot;MessageInspector::AfterReceiveReply(): Got the following reply{0}{1}&quot;, Environment.NewLine, reply);
        }
    
        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            Log.InfoFormat(GetType(), &quot;MessageInterceptor::AfterReceiveRequest(): Got the follwing request{0}{1}&quot;, Environment.NewLine, request);
            return null;
        }
    
        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            Log.InfoFormat(GetType(), &quot;MessageInterceptor::BeforeSendReply(): Sending the following reply{0}{1}&quot;, Environment.NewLine, reply);
        }
    }
    
  2. Erstelle ein weitere Klasse, welche eines der Behavior Interfaces IServiceBehavior, IEndpointBehavior, IOperationBehavior oder IContractBehavior implementiert. Die in Schritt 1 erstellt Klasse wird nun der Liste der Behaviors hinzugefügt.
    public class MessageInspectingBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        public override Type BehaviorType
        {
            get { return typeof(MessageInspectingBehavior); }
        }
    
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            // Ignore
        }
    
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new MessageInspector());
        }
    
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MessageInspector());
        }
    
        public void Validate(ServiceEndpoint endpoint)
        {
            // Ignore
        }
    
        protected override object CreateBehavior()
        {
            return new MessageInspectingBehavior();
        }
    }
    
  3. Der Client oder Service kann nun konfiguriert werden, dieses neue Behavior zu verwenden:
    &lt;system.serviceModel&gt;
        &lt;services&gt;
            &lt;service name=&quot;WcfDemoService.DemoService&quot; behaviorConfiguration=&quot;WcfDemoService.DemoServiceBehavior&quot;&gt;
            ...
                &lt;endpoint address =&quot;&quot;
                          binding=&quot;basicHttpBinding&quot;
                          contract=&quot;WcfDemoService.IDemoService&quot;
                          behaviorConfiguration=&quot;inspectorBehavior&quot; /&gt;
            &lt;/service&gt;
        &lt;/services&gt;
        ...
        &lt;extensions&gt;
            &lt;behaviorExtensions&gt;
                &lt;add name=&quot;messageInspector&quot;
                     type=&quot;WcfLogging.MessageInspectingBehavior, WcfLogging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bb5b60893581ebcd&quot;/&gt;
            &lt;/behaviorExtensions&gt;
        &lt;/extensions&gt;
        &lt;behaviors&gt;
            &lt;endpointBehaviors&gt;
                &lt;behavior name=&quot;interceptorBehavior&quot;&gt;
                    &lt;clientInterceptor /&gt;
                &lt;/behavior&gt;
            &lt;/endpointBehaviors&gt;
        &lt;/behaviors&gt;
    &lt;/system.serviceModel&gt;

Wichtig: Das Assembly, welches das Behavior enthält, muss unbedingt signiert werden!

Das vom Service erzeugte Log enthält nun die über den Service verarbeiteten Nachrichten, sowie Logeinträge der Applikation selber:

2009-10-07 11:51:05,100 [6  ] INFO  - MessageInterceptor::AfterReceiveRequest(): Got the follwing request
&lt;s:Envelope xmlns:s=&quot;http://schemas.xmlsoap.org/soap/envelope/&quot;&gt;
  &lt;s:Header&gt;
    &lt;To s:mustUnderstand=&quot;1&quot; xmlns=&quot;http://schemas.microsoft.com/ws/2005/05/addressing/none&quot;&gt;http://localhost:8731/Design_Time_Addresses/WcfDemoService/DemoService/&lt;/To&gt;
    &lt;Action s:mustUnderstand=&quot;1&quot; xmlns=&quot;http://schemas.microsoft.com/ws/2005/05/addressing/none&quot;&gt;http://tempuri.org/IDemoService/GetData&lt;/Action&gt;
  &lt;/s:Header&gt;
  &lt;s:Body&gt;
    &lt;GetData xmlns=&quot;http://tempuri.org/&quot;&gt;
      &lt;value&gt;42&lt;/value&gt;
    &lt;/GetData&gt;
  &lt;/s:Body&gt;
&lt;/s:Envelope&gt;
2009-10-07 11:51:05,115 [6  ] DEBUG - DemoService::GetData(): Received 42
2009-10-07 11:51:05,116 [6  ] INFO  - MessageInterceptor::BeforeSendReply(): Sending the following reply
&lt;s:Envelope xmlns:s=&quot;http://schemas.xmlsoap.org/soap/envelope/&quot;&gt;
  &lt;s:Header&gt;
    &lt;Action s:mustUnderstand=&quot;1&quot; xmlns=&quot;http://schemas.microsoft.com/ws/2005/05/addressing/none&quot;&gt;http://tempuri.org/IDemoService/GetDataResponse&lt;/Action&gt;
  &lt;/s:Header&gt;
  &lt;s:Body&gt;
    &lt;GetDataResponse xmlns=&quot;http://tempuri.org/&quot;&gt;
      &lt;GetDataResult&gt;You entered: 42&lt;/GetDataResult&gt;
    &lt;/GetDataResponse&gt;
  &lt;/s:Body&gt;
&lt;/s:Envelope&gt;

Ein Beispielprojekt, welches so ein Behavior enthält, kann hier runtergeladen werden: WcfLogging.zip

Die RetryActivity erweitern

5. Oktober 2009

In mein WF Activity Toolkit gehört unter anderem die RetryActivity von Matt Milner. Wie bereits beschrieben kann mit dieser Custom Activity die Ausführung einer Sequenz wiederholt werden, falls innerhalb der Sequenz ein Fehler auftritt. Dabei können die Anzahl der erneuten Versuche und ein Intervall zwischen den Versuchen angegeben werden. Nun habe ich diese Activity noch ein wenig erweitert, um das Intervall zwischen den Versuchen etwas flexibler konfigurieren zu können.

Erweiterte RetryActivity

Erweiterte RetryActivity

Dabei werden zwischen zwei Intervall Modi unterschieden:

  • Linear (IntervalExponential = false)
    Die Zeitabstände zwischen den einzelnen Intervallen sind gleichmässig, zB. immer 1 Minute.
  • Exponentiell (IntervalExponential = true)
    Die Zeitabstände zwischen den einzelnen Intervallen wird immer verdoppelt bis alle Versuche ausgeschöpft sind. Also wird zB. mit einer Sekunde begonnen, und das nächste Intervall dauert zwei Sekunden. Nach 10 Intervall Schritten wird bereits 512 Sekunden gewartet, also etwa achteinhalb Minuten.

Dies kann nützlich sein, wenn zB. ein Aufruf an ein externes System fehlerhaft sein kann, man aber bei vielen parallelen Workflows das externe System nicht durch zahlreiche gleichzeitige Aufrufe belasten will.

Hier ist der der Source Code der erweiterten RetryActivity: RetryActivityEx.zip

3 Nützliche Custom WF Activities

1. Oktober 2009

Nachdem ein grösseres Projekt nun beinahe abgeschlossen ist, komme ich auch wieder dazu, ein paar Erfahrungen der letzten Monate zu beschreiben. Die Basis Library der WF 3 enthält ja einige Activities, welche benutzt werden können, um den Ablauf zu modellieren. In einem typischen Projekt kommen dann sofort eigene Custom Activities dazu, welche sich um die Prozess relevanten Punkte kümmern wie zB. den Zugriff auf eine externe DB.

Folgende 3 Custom Activities haben sich als unentbehrlich erwiesen und gehören fest in mein WF Toolkit:

CallWorkflow

Die InvokeWorkflowActivity der WF kann zwar auch andere Workflows ausführen, sie macht dies jedoch asynchron. Das bedeutet, sofort nach dem Aufruf kehrt die Ausführung in den aufrufenden  Workflow zurück. Die CallWorkflowActivity mit dazugehörigem Runtime Service des WF Spezialisten Jon Flanders bietet die synchrone Variante an. Die Ausführung im aufrufenden Workflow wartet solange, bis die aufgerufene Workflow Instanz beendet ist.

Retry

Die RetryActivity von Matt Milner kann immer dann verwendet werden, wenn der Aufruf einer Activity fehlschlagen kann (werfen einer Exception), und man dann das ganze noch einmal ausführen lassen will. Es kann eine Anzahl der Wiederholungsversuche und ein Intervall zwischen den Versuchen angegeben werden.

RetryActivity

RetryActivity

PersistencePoint

Die letzte Activity ist eine wirklich simple, aber sehr nützliche. Sie kann im Workflow eingesetzt werden, um einen Speicherpunkt im WF Store zu erzwingen auch wenn die WF dies an dieser Stelle nicht von sich aus tun würde. Wenn im Ablauf ein wichtiger Punkt erreicht wurde, kann dies in die Persitenz DB gespeichert werden, und falls der WF unterbrochen wird, fährt die Ausführung beim letzten Speicherpunkt weiter.

Jede Activity, welche mit dem Attribut [PersistOnClose] ausgezeichnet wird, erreicht dieses Verhalten. Die PersistencePointActivity wird somit trivial:

[PersistOnClose]
public partial class PersistencePointActivity : Activity
{
    public PersistencePointActivity()
    {
        InitializeComponent();
    }
}

Setzen der Persistenz Service ID für Workflow Services

29. September 2009

Gestern habe ich einen kleinen, aber nützlichen Hack gezeigt, mit welchem man die Service ID des Persistenzdienstes setzen kann. Dies funktioniert auch ganz prima, solange man nicht mit Workflow Services arbeitet.

Wie man es auch versucht, man erhält keine gültige Referenz auf den Persistenz Service.

Also was tun? Reflector zeigt uns die Interna von WorkflowService.ApplyDispatchBehavior():

static class WorkflowExtensions
...
WorkflowPersistenceService service = item.WorkflowRuntime.GetService&amp;lt;WorkflowPersistenceService&amp;gt;();
if (service != null)
{
    bool isStarted = item.WorkflowRuntime.IsStarted;
    if (isStarted)
    {
        item.WorkflowRuntime.StopRuntime();
    }
    item.WorkflowRuntime.RemoveService(service);
    item.WorkflowRuntime.AddService(new SkipUnloadOnFirstIdleWorkflowPersistenceService(service));
    if (isStarted)
    {
        item.WorkflowRuntime.StartRuntime();
    }
}
...

Was geschieht hier genau?
Wenn via Config oder Code ein Persistenz Service hinzugefügt wurde, wird die Runtime gestoppt. Der Standard Persistenz Service wird entfernt, und ein neuer Service mit dem handlichen Namen SkipUnloadOnFirstIdleWorkflowPersistenceService wird hinzugefügt. Danach wird die Runtime wieder gestartet.
Die Workflow Runtime eines Workflow Services verwendet also intern nicht den SqlWorkflowPersistenceService, sondern einen speziellen, öffentlich nicht zugänglichen und gekapselten Dienst.

Wie also kann nun in einem solchen Kontext die Service ID gesetzt werden?

Die gestern gezeigte Erweiterungsmethode kann wie folgt ergänzt werden:

static class WorkflowExtensions
{
    public static void SetServiceInstanceId(this WorkflowPersistenceService workflowPersistenceService, Guid serviceId)
    {
        Type persistenceServiceType = workflowPersistenceService.GetType();
        FieldInfo instanceIdField = persistenceServiceType.GetField(&amp;quot;_serviceInstanceId&amp;quot;, BindingFlags.NonPublic | BindingFlags.Instance);
        instanceIdField.SetValue(workflowPersistenceService, serviceId);
    }

    public static WorkflowRuntime GetRuntime(this WorkflowServiceHost host)
    {
        return host.Description.Behaviors.Find&amp;lt;WorkflowRuntimeBehavior&amp;gt;().WorkflowRuntime;
    }

    public static T GetWFService&amp;lt;T&amp;gt;(this WorkflowRuntime runtime) where T : class
    {
        if (typeof(T) != typeof(SqlWorkflowPersistenceService))
        {
            return runtime.GetService&amp;lt;T&amp;gt;();
        }
        else
        {
            WorkflowPersistenceService service = runtime.GetService&amp;lt;WorkflowPersistenceService&amp;gt;();
            Type sqps = service.GetType();

            FieldInfo field = (from fieldinfo in sqps.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
                               where fieldinfo.FieldType == typeof (WorkflowPersistenceService)
                               select fieldinfo).First();
            return field.GetValue(service) as T;
        }
    }
}

Mit der Erweiterungsmethode GetRuntime() erhält man die WF Runtime vom WorkflowServiceHost.
Mittels wfRuntime.GetWFService<SqlWorkflowPersistenceService>() kann danach durch den Zugriff via Reflection auf den intern als Feld gekapselten Persistenzdienst zugegriffen werden.

Microsoft WebsiteSpark Programm

28. September 2009

Microsoft unternimmt in letzter Zeit einiges, um ihre Technologien unters (Entwickler-)Volk zu bringen. Letzten Donnerstag habe ich im Blog von Scott Guthrie gelesen, dass Microsoft ein neues Programm namens WebsiteSpark lanciert hat:

WebsiteSpark is designed for independent web developers and web development companies that build web applications and web sites on behalf of others.  It enables you to get software, support and business resources from Microsoft at no cost for three years, and enables you to expand your business and build great web solutions using ASP.NET, Silverlight, SharePoint and PHP, and the open source applications built on top of them.

WebSiteSpark provides software licenses that you can use for three years at no cost.  Once enrolled, you can download and immediately use the following software from Microsoft:

  • 3 licenses of Visual Studio 2008 Professional Edition
  • 1 license of Expression Studio 3 (which includes Expression Blend, Sketchflow, and Web)
  • 2 licenses of Expression Web 3
  • 4 processor licenses of Windows Web Server 2008 R2
  • 4 processor licenses of SQL Server 2008 Web Edition
  • DotNetPanel control panel (enabling easy remote/hosted management of your servers)

Ich war letztes Jahr schon versucht, beim BizSpark Programm mitzumachen, habe es aber dann sein lassen, weil mir die Auskunftspflicht zu lästig war.  Nun jedoch sind die Modalitäten derart lasch, dass ich mich eingeschrieben habe.

Ich besitze eigentlich momentan punkto Lizenzen alles, was ich zum Entwickeln benötige. Expression Sketchflow würde ich hingegen gerne mal ausprobieren, momentan arbeite ich ja mit Balsamiq Mockups. In den Kommentaren zum Posting von Scott Guthrie habe ich dann auch noch gelesen, dass das Programm auch ein Update auf das kommende Visual Studio 2010 beinhaltet. Mal sehen, ob’s klappt :-) .