Dienstag, 18. März 2014

Implementierung eines asynchronen WcfProxy

Vor einiger Zeit zeigte mir ein Kollege eine interessante Implementierung für einen Wcf Client Proxy, die er im Buch Professionell entwickeln mit Visual C# 2012: Das Praxisbuch. von Matthias Geirhos gefunden hat. Ziel war es das Verbindungsmanagement und ExceptionHandling an einer zentralen Stelle (einer statischen Klasse) abzuwickeln.

Leider war die Implementierung speziell für die synchronen Methodenaufrufe über den WebService gedacht, aus diesem Grund habe ich eine TAP-variante implementiert, die  mit async und await arbeitet:
Bitte bedenkt, dass dieser Code nur als Ansatz zu verstehen ist, hier fehlt noch einiges (Exception Handling, Logging, Kann man wirklich con.Close() aufrufen (?), retry connect,...)

    public static class WcfProxy
    {
        public static async Task CallAsync<T>(Action<T> webServiceMethod) where T : ICommunicationObject, new()
        {
            T con = new T();
            try
            {
                // open the connection
                await Task.Factory.FromAsync(con.BeginOpen, con.EndOpen, null);

                // call the method
                await Task.Factory.FromAsync<T>(webServiceMethod.BeginInvoke, webServiceMethod.EndInvoke, con, null);
            }
            catch (CommunicationException comEx)
            {
                // .. do something .. //
                System.Diagnostics.Debug.WriteLine(comEx.ToString());
            }
            finally
            {
                con.Close();
            }
        }

        public static async Task<TResult> CallAsync<T, TResult>(Func<T, TResult> webServiceFunction) where T : ICommunicationObject, new() where TResult : class
        {
            T con = new T();
            TResult res = null;
            try
            {
                // open the connection
                await Task.Factory.FromAsync(con.BeginOpen, con.EndOpen, null);

                // call the method
                res = await Task.Factory.FromAsync<T, TResult>(webServiceFunction.BeginInvoke, webServiceFunction.EndInvoke, con, null);
            }
            catch (CommunicationException comEx)
            {
                // .. do something .. //
                System.Diagnostics.Debug.WriteLine(comEx.ToString());
            }
            finally
            {
                con.Close();
            }
            return res;
        }

    }

Montag, 17. März 2014

Async Methoden im WCF Stub in Portable Class Library (PCL)

Vorbetrachtung

Wenn man mit dem Tool svcutil.exe einen Webservice Stub erstellt, gibt es (wenn man es genau nimmt) 3 verschiedene Arten, wie eine Methode erstellt werden kann:
  1. als synchroner Auftruf, da entspricht der Methodenname dem Namen im WCF-ServiceContract
  2. als asynchroner Aufruf im EAP-Pattern (Event-based Asynchronous Pattern)
    Hierbei gibt es zum einen eine Methode mit dem Suffix -Async, und ein event mit dem Methodennamen und dem Suffix -Completed, außerdem gibt es für jedes Ergebnis einen eigenen EventArgs-Typ 
  3. als asynchroner Aufruf im TAP-Pattern (Task-based Asynchronous Pattern)
    Hier wird auch der Methode ein -Async angehängt, jedoch ist das Ergebnis ein Task mit einem Resulttyp, der dem des Webservice-Result entspricht. Ist die Methode void, gibt es natürlich auch keinen Resulttyp. Der Vorteil dieser Methode ist, dass man kein umständliches Event-Housekeeping machen muss, sondern die Methode fast wie eine synchrone Methode aufrufen kann, ohne dass die Anwendung einfriert

Liest man sich die Beschreibungen durch, erkennt man sicher meine Vorliebe für die TAP-Variante. Diese Variante wird auch ab dem Zielframework .NET 4.5 standardmäßig anstelle von EAP erzeugt. Das ist auch gut so, aber an der Überschrift erkennt man, dass es wohl nicht immer so ist, denn in einer PCL-Bibliothek, die verschiedene Zielplattformen haben kann, wird automatisch Variante 2 erzeugt, was sehr unschön ist, denn für EAP muss man nicht nur unmengen mehr Code schreiben, sondern bei mehreren parallelen Aufrufen auch noch Housekeeping mit State-Objekten

Wenn svcutil.exe uns hier nicht helfen will, müssen wir uns eben selbst helfen:
Nehmen wir folgende WebService:
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        Person GetNextInterviewPartner();
      
        [OperationContract]
        Person GetNextInterviewPartnerGeneric(string typeFullName);

        [OperationContract]
        void AddPersonToList(Person person);
    }

Unabhängig von der Implementierung, oder was der Service eigentlich macht, erhalten wir folgenden Stub (gekürzt):
 public partial class Service1Client {
        
        public void GetNextInterviewPartnerAsync()
        {...}
        
        public System.IAsyncResult BeginGetNextInterviewPartner(System.AsyncCallback callback, object asyncState)
        {...}
        
        public object EndGetNextInterviewPartner(System.IAsyncResult result)
        {...}
        
        public void GetNextInterviewPartnerGenericAsync(string typeFullName)
        {...}
        
        public System.IAsyncResult BeginGetNextInterviewPartnerGeneric(string typeFullName, System.AsyncCallback callback, object asyncState)
        {...}
        
        public object EndGetNextInterviewPartnerGeneric(System.IAsyncResult result)
        {...}
        
        public void AddPersonToListAsync(object person)
        {...}
        
        public System.IAsyncResult BeginAddPersonToList(object person, System.AsyncCallback callback, object asyncState)
        {...}
        
        public void EndAddPersonToList(System.IAsyncResult result)
        {...}

    //[...]
    }

Ich habe den Stub stark gekürzt, und nur die fürs Weitere wichtigen Methoden herausgestellt. Die Events, die bei der standardmäßigen Implementierung notwendig sind, werden nicht benötigt. Auf Basis dieses Stubs kann man einen Proxy implementieren, der auf das TAP-Pattern aufbaut. Dabei hilft die Funktion Task.
  public class Service1ClientTAP : Service1Client
    {
        public new Task<object> GetNextInterviewPartnerAsync()
        {
            return Task.Factory.FromAsync<object>(BeginGetNextInterviewPartner, EndGetNextInterviewPartner, TaskCreationOptions.PreferFairness);
        }

        public new Task<object> GetNextInterviewPartnerGenericAsync(string typeFullName)
        {
            return Task.Factory.FromAsync<string,object>(BeginGetNextInterviewPartnerGeneric, EndGetNextInterviewPartnerGeneric, typeFullName,  TaskCreationOptions.PreferFairness);
        }

        public new Task AddPersonToListAsync(object person)
        {
            return Task.Factory.FromAsync<object>(BeginAddPersonToList, EndAddPersonToList, person, TaskCreationOptions.PreferFairness);
        }
    }
Das Wichtige sind die beiden delegates Begin... und End..., diese werden in der Funktion Task.Factory.FromAsync als erstes übergeben, danach können bis zu 3 Übergabeparameter folgen. Danach werden die TaskCreationOptions übergeben. Normalerweise muss die Methode nicht explizit typisiert werden, wenn es sich bei den Parametern und Resultaten nicht unbedingt um object-Typen handelt. Hier nochmal zusammengefasst, wie die einzelnen Funktionssignaturen überführt werden:
1. TResult TestFunktion1(T1 parameter1, T2 parameter2, T3 parameter3)
2. TResult TestFunktion2()
3. void TestFunktion3(T1 parameter1, T2 parameter2, T3 parameter3)
// 1.
public Task<TResult> TestFunktion1Async(T1 parameter1, T2 parameter2, T3 parameter3)
{
   return Task.Factory.FromAsync<T1, T2, T3, TResult> (BeginTestFunktion1, EndTestFunktion2, parameter1, parameter2, parameter3, TaskCreationOptions.PreferFairness);
}

// 2.
public Task<TResult> TestFunktion2Async()
{
   return Task.Factory.FromAsync<TResult> (BeginTestFunktion1, EndTestFunktion2, TaskCreationOptions.PreferFairness);
}

// 3.
public Task TestFunktion3Async(T1 parameter1, T2 parameter2, T3 parameter3)
{
   return Task.Factory.FromAsync<T1, T2, T3> (BeginTestFunktion1, EndTestFunktion2, parameter1, parameter2, parameter3, TaskCreationOptions.PreferFairness);
}

Montag, 24. Februar 2014

Generische Methoden in WCF Webservice

Manchmal wünscht man sich von einem WCF Service, dass er die Eier legende Wollmilchsau ist, die er niemal sein kann. Daher für alle Leser, die hier eine Step by Step Anleitung für das erwarten, was im Titel steht, sage ich gleich: ES GEHT NICHT! Und ich sag das nich einfach so, sondern möchte hier kurz 3 Gründe nennen, die mir spontan einfallen, warum es nicht geht. Aber erst möchte ich kurz erörtern, wozu man generische Methoden verwenden könnte, mir ist spontan nichts konkretes eingefallen, daher habe ich in der Suchmaschine meines Vertrauens, folgende Methode gefunden:

public T Get<T> Max(List<T> listOfObjects) where T: IComparable<T>

Diese Funktion gibt ein Objekt vom Typ T zurück, welches das Größte aus der gegebenen Liste ist. Klingt erstmal ganz gut, aber wenn wir in Richtung einer WCF-Implementierung denken, kommen ziemlich schnell Fragen auf, die eine Implementierung so unmöglich machen.


  1. Wie serialisieren wir den Typparameter? Schließlich soll der Service auch aus anderen Betriebssystemen / Programmiersprachen bedient werden können als C#.
  2. Interfaces werden über WCF Webservices ignoriert (im Beispiel IComparable), da sie ja nicht serialisiert/deserialisiert werden können. In gegebenem Fall beschreibt das Interfaces nur Methoden, die implementiert sein müssen, hat also nichts mit den Daten zu tun.
  3. und wenn das noch nicht überzeugt hat: Im wsdl (bzw. xsd) für den Service müssen alle Datentypen beschrieben sein, die über den Transportweg als Xml gesendet werden können. Im Fall des Parametertyps T, wären das alle Typen des .NET-Frameworks. Wer sich schonmal die Definition eines normalen WCF-Services angesehen hat, wird zugeben, dass das vermutlich den Rahmen sprengt.

Überzeugt? Dann will ich Euch gern einen Weg zeigen, wie man in einem konkreten Beispiel einen auch über Webservices ähnlichen erfolg erzielen kann. 

Das Beispiel

Nehmen wir einen Webservice, der es einem Abteilungsleiter ermöglichen soll, seine Mitarbeitergespräche zu verwalten (inkl. Vorstellungsgespräche). Dazu gibt es eine Funktion für das Seketariat, welches einen Mitarbeiter/Bewerber in eine Liste aufnimmt. In einer generischen Methode würde das so aussehen:

void AddPersonToList<T>(T person) where T:IPerson;

Für den Abteilungsleiter soll der Service eine Funktion zur Verfügung stellen, welche es ihm ermöglicht einen bestimmten Typ aus der Liste abzufragen. So ist es möglich zum Beispiel, den nächsten Bewerber zu erhalten, oder eben den nächsten Angestellten, oder gar einen speziellen Angestellten, zum Beispiel einen Teamleiter:

T GetNextPerson<T>() where T:IPerson;

Wrapper-Funktionen im ServiceContract

Ein Webservice kann intern eine generische Funktion verwenden, jedoch nicht nach außen transportieren. Eine Möglichkeit wäre also für jeden Typ eine Funktion nach außen zur Verfügung zu stellen, die entsprechend intern diese generische Methode aufruft. Je nachdem, wie viele Typen es gibt, muss eine separate Funktion zur Verfügung gestellt werden:

// secretary functions
public void AddApplicant(Applicant person)
{
   AddPersonToList<Applicant>(person);
}
public void AddEmployee(Employee person) {..}
public void AddSapEmployee(SapEmployee person) {..}
public void AddNetDeveloper(NetDeveloper person) {..}
[..]

// manager functions
public Applicant GetNextApplicant()
{
   return GetNextPerson<Applicant>();
}

public Employee GetNextEmployee() {..}
public SapEmployee GetNextSapEmployee() {..}
public NetDeveloper GetNextNetDeveloper() {..}
[..]
Für den ersten Schuss ist die Idee ganz gut, aber mit Sicherheit nicht ideal, zumindest muss man dadurch keine Code-Dubletten schreiben, sondern kann die bestehende Implementierung verwenden.

Verwendung der Basistypen im OperationContract

Wie man schnell sieht, implementieren in unserem Fall alle Personen das Interface IPerson. Unsere erste generische Funktion können wir also unter Verwendung des Interface etwas generalisieren:

void AddPersonToList(IPerson person);

In diesem Fall entspricht der Funktionsaufruf syntaktisch sogar dem Original. Es gibt aber immer noch ein Problem, denn wir wissen nicht, welche Implementierungen von IPerson in Frage kommen. Damit kann der Webservice auch keine valide Schnittstellenbeschreibung für eine mögliche Clientimplementierung zur Verfügung stellen. Um genau zu sein, würde es so erst zur Laufzeit zu einem Fehler kommen. Ruft man zum Beispiel in der Clientanwendung AddPersonToList(new SapDeveloper()) auf, bekommt man folgenden Fehler:





Der Fehlerdetails an sich erschließen sich einem im ersten Moment nicht:
Zusätzliche Informationen: Fehler beim Deserialisieren von Parameter http://tempuri.org/:person. Die InnerException-Nachricht war "Der Typ 'Test.Model.SapDeveloper' mit dem Datenvertragsnamen 'SapDeveloper:http://schemas.datacontract.org/2004/07/Test.Model' wird nicht erwartet. Verwenden Sie ggf. einen DataContractResolver, oder fügen Sie alle unbekannten Typen statisch der Liste der bekannten Typen hinzu, beispielsweise mithilfe des KnownTypeAttribute-Attributs oder indem Sie sie zur Liste der bekannten Typen hinzufügen, die an DataContractSerializer übergeben wird.".  Weitere Details finden Sie unter "InnerException".


Der Client könnte theoretisch ein Objekt der Klasse SapDeveloper serialisieren, dies würde sogar der Schnittstellenbeschreibung entsprechen, denn der Soap-Service definiert den Übergabeparameter als anyType


Jedoch gibt die Schnittstellenbeschreibung nicht wirklich vor, welche Implementierung verwendet werden soll, bzw. welche denn der Webservice verstehen würde. Auch wenn wir es also schaffen würden, die Serialisierung des Objekts der Klasse SapDeveloper auf Client Seite hinzubekommen, könnte der Server damit nicht viel anfangen. 

Der Akteur ist also immer derjenige, der die Schnittstelle vorgibt - also der Webservice. Aber die Fehlermeldung gibt noch mehr Anhaltspunkte, wie wir weiter machen können,es gibt ein KnownType-Attribut, welches man verwenden kann, um eine Liste möglicher Implementierungen anzugeben. Dieses Attribut wird verwendet, um einem DataContractSerializer zu sagen, welche Implementierungen dieser serialisieren bzw. deserialisieren "darf". Leider verwenden wir den DataContractSerializer nur implizit, denn das macht der WCF Service für uns. Daher gibt es das Attribut ServiceKnownType, welches man direkt am Service-Interface definieren kann, um mögliche Implementierungen Service-weit zu definieren. In unserem Fall sieht die fertige Servicedefinition so aus:


    [ServiceKnownType(typeof(Test.Model.Person))]
    [ServiceKnownType(typeof(Test.Model.Employee))]
    [ServiceKnownType(typeof(Test.Model.SAPEmployee))]
    //[ServiceKnownType("GetKnownTypes", typeof(Helper))]
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        IPerson GetNextInterviewPartner();

        
        // so geht das nicht
        //[OperationContract]
        //T GetNextInterviewPartnerGeneric() where T : IPerson;

        [OperationContract]
        IPerson GetNextInterviewPartner(string typeFullName);

        [OperationContract]
        void AddPersonToList(IPerson person);

     
    }
Wie man leicht sieht, gibt es zwei Varianten das Attribut zu verwenden, entweder man gibt die Typen direkt hintereinander an, oder einen Verweis auf eine Methode, die ein IEnumerable<Type> zurückgibt.

Eigentlich ist damit die Aufgabe gelöst, nur gibt es einen unschönen Nebeneffekt. Da es in der Schnittstellenbeschreibung keine Interfaces gibt, werden diese immer als anyType interpretiert, in einem C#-Stub ist dies dann der Typ object. Das führt dazu, dass jede Art von Typisierungsfehlern erst zur Laufzeit auffallen. Um das einzudämmen, sollte man stattdessen entweder mit einer Basisimplementierung als Parametertyp arbeiten oder eine abstrakte Klasse zur Schnittstellenbeschreibung verwenden.

Donnerstag, 18. Juli 2013

Codeausschnitte im Visual Studio einbinden

Codeausschnitte kennt bestimmt jeder, ich habe ja hier in meinem Blog schon viele gepostet, die man einfach ausschneiden und in sein eigenes Programm übernehmen kann. Dabei handelt es sich meist um ganze Klassen oder komplexe Funktionen. Während des Programmierens verwendet man oft viel kleinere Codeausschnitte immer und immer wieder. Mit Hilfe der IntelliSense-Technlogie, sind diese Snippets auch recht schnell runter getippt. Beispiele gibt es viele: ein try-catch-finally-Blöcke, if-else, Properties, you name it... Und viele von uns verwenden auch die Snippet-Funktion im Visual Studio - Gibt man zum Beispiel "propfull" mit folgendem Tabulator ein, wird automatisch eine Vorlage für ein Property erstellt:
|  |
\ /

Jetzt muss man nur noch die markierten Bezeichner eingeben (Variablentyp, Feldname, Propertyname) und man hat ein Property erstellt. Das ist ja schön und gut, und auch nützlich. Aber selbst sowas machen? Oft wird die Antwort sein "Brauch ich jetzt nicht", "Da muss ich mich erst einarbeiten, da hab ich keine Zeit für".. Aber als Softwareentwickler sollten wir es doch besser wissen: Investiert man einmal ein wenig mehr Zeit, kann man in Zukunft "faul" sein.

Ich möchte Euch am Beispiel eines Snippets für ein ViewModelProperty kurz zeigen, wie es funktioniert. Zu erst benötigen wir das Code Snippet als C# Code:

private string _testField;

/// <summary>
/// gets / sets  TestProperty
/// </summary>
public string TestProperty
{
  get { 
      return _testField; 
  }
  set {
      if (value != _testField)
      {
        _testField = value;
        OnPropertyChanged(() => TestProperty);
      }
  }
}

So weit, so gut. Nachdem wir festgelegt haben, dass die Bezeichnung für das snippet propvm sein soll, müssen wir jetzt definieren, welche Teile des Codes vom Benutzer eingegeben werden, diese müssen wir dann durch ein Literal mit der Syntax $ID$ ersetzen. In unserem Fall sind das die Teile "string", "_testField" und "TestProperty":
private $type$ $field$;

/// <summary>
/// gets / sets  $property$
/// </summary>
public $type$ $property$
{
  get { 
      return $field$; 
  }
  set {
      if (value != $field$)
      {
        $field$= value;
        OnPropertyChanged(() => $property$);
      }
  }
}
Das war schon fast alles, jetzt müssen wir das ganze nur noch in das snippet-Xml-Format einfügen, ein paar Metadaten dazu und fertig sind wir. Wichtig dabei ist, dass wir jedes Literal, das wir verwenden im Xml auch deklarieren und mit einem netten Tooltip für den Anwender versehen. Das fertige Snippet sieht dann also so aus:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
 <CodeSnippet Format="1.0.0">
  <Header>
   <Title>propvm</Title>
   <Shortcut>propvm</Shortcut>
   <Description>Codeausschnitt für eine ViewModel-Eigenschaft inkl. dem dahinter liegenden Feld</Description>
   <Author>Thomas Kison</Author>
   <SnippetTypes>
    <SnippetType>Expansion</SnippetType>
   </SnippetTypes>
  </Header>
  <Snippet>
   <Declarations>
    <Literal>
     <ID>type</ID>
     <ToolTip>Eigenschaftentyp</ToolTip>
     <Default>string</Default>
    </Literal>
    <Literal>
     <ID>property</ID>
     <ToolTip>Eigenschaftenname</ToolTip>
     <Default>MyProperty</Default>
    </Literal>
    <Literal>
     <ID>field</ID>
     <ToolTip>Die Variable hinter dieser Eigenschaft</ToolTip>
     <Default>_myVar</Default>
    </Literal>
   </Declarations>
   <Code Language="csharp">
        <![CDATA[private $type$ $field$;

/// <summary>
/// gets / sets  $property$
/// </summary>
public $type$ $property$
{
  get { 
      return $field$; 
  }
  set {
      if (value != $field$)
      {
        $field$= value;
        OnPropertyChanged(() => $property$);
      }
  }
}
 $end$]]>
   </Code>
  </Snippet>
 </CodeSnippet>
</CodeSnippets>
Jetzt müssen wir Visual Studio nur noch mitteilen, dass es dieses Script gibt. Das geht recht einfach, wir legen es in unser "Snippet-Verzeichnis" von Visual Studio. Den genauen Pfad finden wir unter Extras > Codeausschnitt-Manager unter dem Ordner My Code Snippets



Viel Spaß mit Euren eigenen Visual Studio Snippets!

Code Snippet: ViewModelBase

Oft benötige ich eine Implementierung der ViewModelBase-Klasse, dann muss ich diese entweder aus anderen Projekten kopieren, oder googeln. Jetzt muss ich das nicht mehr, denn ich merk sie mir einfach hier:

// parts of this code is based upon the article : http://msdn.microsoft.com/de-de/magazine/dd419663.aspx#id0090051
class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// raises the PropertyChanged event with the given string parameter
    /// </summary>
    /// <param name="propertyName">name of the property, that has changed</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        // just to avoid wrong Event-Calls (only in debug mode)
        this.VerifyPropertyName(propertyName);

        // call the event handler
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }

    /// <summary>
    /// raises the PropertyChanged-event, with the name of the property that is given by the lambda expression
    /// </summary>
    /// <typeparam name="Tprop">type parameter of the given property, to be ignored</typeparam>
    /// <param name="property">the lambda expression f.e.: "() => TestProperty"</param>
    protected virtual void OnPropertyChanged<Tprop>(Expression<Func<Tprop>> property)
    {
        // cast as LambdaExpression
        LambdaExpression lambda_exp = (LambdaExpression)property;
        if (lambda_exp != null) 
        {
            MemberExpression member_exp = (MemberExpression)lambda_exp.Body;
            if (member_exp != null)
            {
                OnPropertyChanged(member_exp.Member.Name);
                return;
            }
        }
        Debug.Fail("No correct LambdaExpression provided for OnPropertyChanged.");
    }

    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyPropertyName(string propertyName)
    {
        // Verify that the property name matches a real,  
        // public, instance property on this object.
        if (TypeDescriptor.GetProperties(this)[propertyName] == null)
        {
            string msg = "Invalid property name: " + propertyName;
            Debug.Fail(msg);
        }
    }
}

Mittwoch, 6. Februar 2013

Build-Ampel selbst bauen

Motivation

Selbst für kleine Projekte, an denen nur eine Hand voll Mitarbeiter beteiligt sind, wird man schnell die Vorteile eines Build-Servers schätzen lernen. Eine Funktion eines Build-Servers ist es anzuzeigen, ob das, was man gerade in die Quellcodeverwaltung "commited" hat auch auf einem Fremdsystem gebuildet werden kann. Zur Anzeige des Build-Status auf dem Entwicklungsrechner werden oft Programme verwendet, die den Status als Ampel-Icon im Tray anzeigen.

Als Beispiel möchte ich den Build-Server ThoughtWorks Cruise Control .Net erwähnen, hierfür existiert das Tool CCTray, welches ein Tray-Icon zur Verfügung stellt, um den Status anzuzeigen. Da ich mitbekommen habe, dass dieses kleine Icon von Entwicklern oft ignoriert wird, habe ich nach einer anderen Lösung gesucht und auch gefunden. 

Was wäre wenn man diese Software-Ampel aus dem Tray auf eine reale Ampel überträgt !? Naja, vielleicht ist eine echte Verkehrsampel etwas übers Ziel hinaus geschossen, aber da hat mein einjähriger Sohn auf die Richtige Idee gebracht: Für sein Bobby Car hat er eine Spiel-Ampel von BIG, die fürs Erste ganz gut geeignet für meine Anforderung ist.

Fehlt nur noch die Steuerung der Ampel, man könnte hier direkt über ein USB Relais-Interface die einzelnen Lampen an der Ampel ansteuern und schalten. An sich ein probates Mittel, aber so recht keine Herausforderung - ich denke da eher an eine Funk-Fernsteuerung oder übers Netzwerk. Da ich nach langem Suchen keine praktikable Lösung gefunden habe, wollte ich schon fast aufgeben, dann ist mir aber ein Angebot von Pollin Electronic ins Auge gesprungen, der Bausatz AVR-NET-IO. Damit kann man über TCP/IP bis zu 8 Ausgänge schalten, das sollte für 2 Ampeln und 2 Fußgängerampeln genügen.

Einkaufsliste

Bobby Car Ampel von Big (13,99 Euro)
Bausatz: AVR-NET-IO (19,95 Euro)
Lötkolben, Lötzinn, Werkzeug, .. (sollte man zu Hause haben)
Kabel / min. 12-adrig, ca.1 m (zB: billiges Scart-Kabel: ca. 1 Euro)
Netzteil 9V~ min. 200 mA  (ab 2 Euro)

Gesamtpreis: 36,94 Euro (zzgl. Versand)

Umsetzung

Quelle: Wikipedia, http://en.wikipedia.org/wiki/Parallel_port
Zuerst sollte man den AVR-NET-IO Bausatz nach der Anleitung zusammen löten, für uns interessant ist hier prinzipiell der DB-25F Port (J3), dieser entspricht in den Pins 2-13 und den GND-Pins 18-25 einem herkömmlichen LPT Port, was es theoretisch ermöglich hier eine entsprechende parallele Interface-Karte anzuschließen und anzusteuern. Speziell wenn man vor hat größere Ströme zu schalten, macht dies Sinn. Die Leuchtmittel in unserer Spielzeugampel kommen jedoch mit dem Steuerstrom von 5 Volt ganz gut aus, so können wir uns die 15 Euro für die Relaiskarte sparen. Wer es modular aufbauen will, kann eine entsprechende Anschlussplatine kaufen (ca. 4 Euro), um die einzelnen Ausgänge an  die Ampel anzuschließen.

Nachdem man nun die Ampel durch die Schraube an der Unterseite auseinander genommen hat und die Drähte von der Ampel-Steuerplatine trennt, wird man feststellen, dass man mehr Kabel hat, als Adern im Kabel. Jede Lampe an der Ampel hat 2 Adern, die abgehen - dabei handelt es sich zum einen um das GND-Kabel (also die Masse) und zum Anderen um die Phase. Da es sich in gegebenem Fall um Gleichstrom handelt, ist das eine der Pluspol, der andere der Minuspol. Aktuell ist irrelevant, was was ist, denn in unserer Ampel befinden sich Glühbirnen. Die GND-Kabel von den zwei Rot-Gelb-Grün Ampeln können zusammengefasst werden und durch eine Ader zum AVR-Modul geführt werden. Außerdem kann man die Grünen GND-Kabel auch da mit zusammenfassen. Zusätzlich brauchen wir ein "Kabelverbund", der immer Spannung erhält, dazu gehört jeweils ein Kabel der roten Lampe aus den Fußgängerampeln.

Jedes übrige Kabel (also die Restlichen 10) muss in einer separaten Ader zum AVR-IO-Modul geführt werden. In meinem Aufbau habe ich das AVR-IO-Modul unten auf den Sockel der Ampel aufgeschraubt und das Kabel durch die hohle Stange der Ampel geführt.Prinzipiell würde das Modul auch direkt in die Ampel passen (dann würde man sich das 12-Adrige Kabel sparen), aber da das Modul recht heiß wird, finde ich diese Lösung nicht sehr praktikabel.

Nachdem wir nun alle Kabel am Modul haben, können wir uns der Pin-Belegung widmen. Das GND-Kabel wird an eine der GND-Leitungen angeschlossen, wie es am Besten passt. Der andere "Kabelverbund" wird an die High-Leitung (5V), also Pin 15 angeschlossen.

Ampel 1
Grün: Pin 2 (D0)
Gelb: Pin 3 (D1)
Rot: Pin 4 (D2)

Ampel 2
Grün: Pin 5 (D3)
Gelb: Pin 6 (D4)
Rot: Pin 7 (D5)

Nun haben wir noch 4 Adern übrig, aber nur 2 Ausgänge. Wenn man etwas darüber nachdenkt, kommt man auf die Lösung - Die Kabel für rot und grün werden gemeinsam angeschlossen. Da die zweite Ader für Rot auf 5V und die zweite Ader für Grün auf GND liegt, ist die Ausgabe der Fußgängerampel abhängig von der jeweiligen Datenleitung, liegt diese auf GND, so leuchtet sie rot, liegt sie auf HIGH (5V), so leuchtet sie grün.

Ampel 3
Grün: Pin 8 (D6)
Rot: Pin 8 (D6)

Ampel 4
Grün: Pin 9 (D7)
Rot: Pin 9 (D7)

Steuerung & Software

Jetzt könnten wir eigentlich schon mal die Ampel testen, der Anschluss an einen PC erfolgt über ein Cross-Over Netzwerkkabel, am PC muss man dafür einfach die Adaptereinstellungen der Netzwerkkarte so ändern, dass man dem PC eine IP aus dem Subnetz 192.168.0.0/24 zuweist, zum Beispiel:
IP: 192.168.0.100
Subnetzmaske: 255.255.255.0

Die IP-Adresse des AVR-NET-IO ist 192.168.0.90, der TCP-Port für eine Verbindung ist  50290 . Verbinden kann man sich nun mit Hilfe von Telnet oder PuTTY (siehe Beschreibung des Bausatz) und die ersten Befehle testen. Für uns relevant ist lediglich der Befehl SETPORT. Mit Hilfe von SETPORT 1.1 kann man nun zum Beispiel die grüne Lampe an Ampel 1 aufleuchten lassen. Aber Vorsicht beim Testen, wenn ihr keine Relaisplatine verwendet können maximal etwa 6 Lampen gleichzeitig leuchten, sonst bekommt der Mikrocontroller nicht mehr genug Strom, um die Netzwerkkommunikation aufrecht zu erhalten.

Kommen wir nun zur Software in C#, um die Ampel anzusteuern. Die erste Abstraktionsschicht bildet die Klasse AVRNetIO:

    class AVRNetIO
    {
        /// <summary>
        /// Pin Status
        /// </pre>
        public enum AVRNetIOStatus
        {
            /// <summary>
            /// 5V
            /// </summary>
            High=1,
            /// <summary>
            /// GND
            /// </summary>
            Low=0
        }

        static System.Net.Sockets.TcpClient _client = new System.Net.Sockets.TcpClient(Properties.Settings.Default.NetIoHost, Properties.Settings.Default.NetIoPort);

        /// <summary>
        /// sets the Portstatus of a connected AVR-NET-IO modul
        /// </summary>
        /// <param name="pinId" />the pin id (1-8)
        /// <param name="status" />the status (high|low)
        public static void SetOutput(int pinId, AVRNetIOStatus status)
        { 
            if (pinId<1 data-blogger-escaped-pinid="">8)
                throw new NotImplementedException("The AVR-NET-IO only implements 8 output pins (1-8)")
            System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
            string command = "SETPORT "+pinId+"."+(int)status+"\r\n";
            _client.Client.Send(enc.GetBytes(command));
            System.Threading.Thread.Sleep(100);
        }
    }
Nun ist es möglich das Modul übers Netzwerk direkt anzusteuern und die Ausgänge zu schalten. IP und Port sind hier per AppConfig einstellbar.

Kommen wir nun zur nächsten Abstraktionsebene, die Ampel an sich. Dazu gibt es erst einmal einen enum für den Ampelstatus und einen für die Art der Ampel (Fußgänger, Verkehr):

        public enum SignalLightTypes
        {
            TrafficLight,
            PedestrianLight
        }

        public enum SignalLightSignals
        {
            Red=2,
            Yellow=4,
            Green=8
        }

Die eigentliche Klasse für SignalLight wird folgendermaßen implementiert:

    public class SignalLight
    {
        SignalLightTypes _type;
        int _startPin;

        public SignalLight(SignalLightTypes type, int startPinId)
        {
            _startPin = startPinId;
            _type = type;
        }

        void SetStatus(SignalLightSignals signalStatus)
        {
            // we only use 1 pin for the pedestrian light
            if (_type == SignalLightTypes.PedestrianLight)
            {
                 if ((signalStatus & SignalLightSignals.Red) == SignalLightSignals.Red)
                     AVRNetIO.SetOutput(_startPin, AVRNetIO.AVRNetIOStatus.Low);
                 if ((signalStatus & SignalLightSignals.Green) == SignalLightSignals.Green)
                     AVRNetIO.SetOutput(_startPin, AVRNetIO.AVRNetIOStatus.High);
            }
            else
            {
                // startPin : green
                // startPin+1 : yellow
                // startPin+2 : red
                AVRNetIO.AVRNetIOStatus redStatus = AVRNetIO.AVRNetIOStatus.Low;
                AVRNetIO.AVRNetIOStatus yellowStatus = AVRNetIO.AVRNetIOStatus.Low;
                AVRNetIO.AVRNetIOStatus greenStatus = AVRNetIO.AVRNetIOStatus.Low;
                if ((signalStatus & SignalLightSignals.Red) == SignalLightSignals.Red)
                    redStatus = AVRNetIO.AVRNetIOStatus.High;
                if ((signalStatus & SignalLightSignals.Yellow) == SignalLightSignals.Yellow)
                    yellowStatus = AVRNetIO.AVRNetIOStatus.High;
                if ((signalStatus & SignalLightSignals.Green) == SignalLightSignals.Green)
                    greenStatus = AVRNetIO.AVRNetIOStatus.High;
                int pinId = _startPin;
                AVRNetIO.SetOutput(pinId, greenStatus);
                AVRNetIO.SetOutput(++pinId, yellowStatus);
                AVRNetIO.SetOutput(++pinId, redStatus);
            }
        }

        private SignalLightSignals _currentStatus;

        /// <summary>
        ///  gets/sets the current status of the signal light
        /// </summary>
        public SignalLightSignals CurrentStatus
        {
            get { return _currentStatus; }
            set { 
                _currentStatus = value;
                SetStatus(value);
            }
        }

    }
Kein Hexenwerk, je nachdem welcher enum als CurrentStatus gesetzt wird, wird der / die entsprechenden Pins auf High gesetzt. Bei Fußgängerampeln ist es ein kleiner Sonderfall, hier wird nur ein Pin verwendet. Bei rot wird dieser auf Low und bei grün auf High geschalten. Initial sind die Fußgängerampeln immer auf rot, denn die Pins vom AVR-Net-IO sind initial auf Low geschalten.

Wir könnten jetzt anhand dessen eine "sinnvolle" Ampelsteuerung entwickeln, das wollen wir aber nicht, denn der Initiale Gedanke war es, eine Build-Ampel zu entwickeln. Ich habe anfangs von CCTray bzw. CC.Net gesprochen, daher will ich jetzt eine Beispielimplementierung für CC.Net zeigen. Dazu brauchen wir die Datei ThoughtWorks.CruiseControl.Remote.dll aus dem Setup von CCTray, da ich nicht weiß in wie weit es Distributionsbeschränkungen gibt, gebe ich hier nur den Namen der Datei an, der Download muss dann direkt von der Seite von CC.Net geschehen (im Bundle mit der Software CCTray)

Um nun den Status aus CruiseControl.Net auszulesen, müssen wir eine neue Instanz von CruiseServerHttpClient erstellen, damit können wir nun den Projektstatus von jedem Projekt auslesen (GetProjectStatus()) und in den Ampelstatus umwandeln.


// initiate signal light classes
SignalLight _signal1 = new SignalLight(SignalLight.SignalLightTypes.TrafficLight, 1);
SignalLight _signal2 = new SignalLight(SignalLight.SignalLightTypes.TrafficLight, 4);
SignalLight _signal3 = new SignalLight(SignalLight.SignalLightTypes.PedestrianLight, 7);
SignalLight _signal4 = new SignalLight(SignalLight.SignalLightTypes.PedestrianLight, 8);

// get variables from config
var ipAddressOrHostNameOfCCServer = Settings.Default.buildserver; 
string project1 = Settings.Default.project1;
string project2 = Settings.Default.project2;
string project3 = Settings.Default.project4;
string project4 = Settings.Default.project4;
var client = new CruiseServerHttpClient(
    string.Format("http://{0}/ccnet/", ipAddressOrHostNameOfCCServer));

foreach (var projectStatus in client.GetProjectStatus())
{ // iterate through projects
    SignalLight.SignalLightSignals status;

    // convert project's status to SignalLightsSignals
    // building -> yellow, success -> green, failure -> red, everything else -> yellow
    if (projectStatus.Activity == ProjectActivity.Building)
        status = SignalLight.SignalLightSignals.Yellow;
    else if (projectStatus.BuildStatus == IntegrationStatus.Success)
        status = SignalLight.SignalLightSignals.Green;
    else if (projectStatus.BuildStatus == IntegrationStatus.Failure || projectStatus.BuildStatus == IntegrationStatus.Exception)
        status = SignalLight.SignalLightSignals.Red;
    else
        status = SignalLight.SignalLightSignals.Yellow;

    // set status on appropriate project
    if (projectStatus.Name == project1)
    {
        _signal1.CurrentStatus = status;
    }
    if (projectStatus.Name == project2)
    {
        _signal2.CurrentStatus = status;
    }
    if (projectStatus.Name == project3)
    {
        _signal3.CurrentStatus = status;
    }
    if (projectStatus.Name == project4)
    {
        _signal4.CurrentStatus = status;
    }
}

Bitte seht mir nach, dass ich momentan keine Bilder habe, sobald ich Zeit finde, werde ich ein paar Bilder machen und diese posten.

Mittwoch, 24. Oktober 2012

SQL Code Snippet: Anzeigen aller durch Fremdschlüssel abhängigen Tabellen

Mit diesem MS SQL-Script ist es möglich zu einer gegebenen Tabelle alle Tabellen zu finden, von denen diese durch Foreign Key Constraints abhängig ist.
declare @TableName nvarchar(50)='TestTable2'
declare @SchemaName nvarchar(10)='dbo'

 select schRef.name+'.'+referenced.name from sys.foreign_keys fk
 join sys.tables referenced on referenced.object_id = fk.referenced_object_id
 join sys.tables base on base.object_id = fk.parent_object_id
 join sys.schemas schBase on schBase.schema_id = base.schema_id
 join sys.schemas schRef on schRef.schema_id = referenced.schema_id
 where base.name=@TableName and schBase.name=@SchemaName
um das ganze jetzt auch noch vollständig ausgeben zu können (inkl. Column), wäre folgendes der Code:
declare @TableName nvarchar(50)='TestTable2'
declare @SchemaName nvarchar(10)='dbo'

  select 
 schRef.name+'.'+referenced.name  ReferencedTable
 , colRef.name      ReferencedColumn
 , schBase.name+'.'+base.name  BaseTable
 ,colBase.name      BaseColumn
 from sys.foreign_keys fk
  -- ref table
 join sys.tables referenced on referenced.object_id = fk.referenced_object_id
 join sys.schemas schRef on schRef.schema_id = referenced.schema_id
 
 -- base table
 join sys.tables base on base.object_id = fk.parent_object_id
 join sys.schemas schBase on schBase.schema_id = base.schema_id
 
 -- columns
 join sys.foreign_key_columns fkc on fkc.constraint_object_id = fk.object_id
 
 join sys.all_columns colBase on colBase.column_id = fkc.parent_column_id and colBase.object_id = base.object_id
 join sys.all_columns colRef on colRef.column_id = fkc.referenced_column_id and colRef.object_id = referenced.object_id


 where base.name=@TableName and schBase.name=@SchemaName

Samstag, 13. Oktober 2012

Große XML Datei in mehrere kleine Dateien aufteilen

Problembeschreibung


Wer viel mit XML-Dateien zu tun hat, kennt vielleicht das Problem - man hat gerade im Programm (oder aus einer Datenbank) ein XML erzeugt, das witzige 50 MB groß ist. Eigentlich in der heutigen Zeit von Terabyte-Festplatten kein großer Wert, sollte man meinen, aber Visual Studio hängt sich beim Öffnen auf. Auch Notepad++ hat hier selbst mit SSD bedenkliche Probleme. Einzig das gute alte Notepad von Windows kann die Datei zumindest anzeigen - das war's aber dann auch, denn damit kann ich nicht wirklich viel anfangen, immerhin sind 50MB auf einer Zeile nicht wirklich übersichtlich.

Lösung

Ich habe mir ein kleines Programm geschrieben, dass ein Eingabe XML in mehrere kleinere XML aufteilt. Die Herausforderung lag darin die Datei nicht komplett einzulesen, sondern Knoten für Knoten zu verarbeiten. Außerdem musste sich das Porgramm merken, welche Knoten noch geöffnet sind, um diese dann zu schließen, sobald die maximale Anzahl an Bytes erreicht wurde. Dadurch ist gewährleistet, dass die resultierenden XML-Dateien auch valides XML repräsentieren.
 

Implementierung

Im Prinzip verwende ich für das Lesen der XML-Datei einen XmlTextReader , das hat den entscheidenden Vorteil, dass die XML-Elemente einzeln eingelesen werden und man XML spezifische Metainformationen zu jedem gelesenen Element erhält. Ich spar mir im Code das kopieren der Kommentare und der XML-Deklaration, wenn das jemand benötigt, ist das schnell zu ergänzen. Außerdem verzichte ich auf die Berücksichtigung von Namespaces.
 
 Der  Programmablauf ist recht einfach:
- Lese jeden Tag und puffere diesen
- öffnende Tags werden auf einen Stack gelegt
- bei schließenden Tags wird der letzte öffnende Tag vom Stack entfernt
- hat der Puffer die gewünschte Größe, so schließe alle noch offenen Tags und schreibe den Puffer auf Platte
- leere Puffer
- öffne die zuletzt offenen Tags wieder im aktuellen Puffer
 
Und hier nun der Code:
 
 
    class XmlUtility
    {
        #region XmlOpeningTag class
        class XmlOpeningTag
        {
            
            public XmlOpeningTag(string name, string fullOpeningTag)
            {
                TagName = name;
                FullOpeningTag = fullOpeningTag;
            }
            public string TagName { get; set; }
            public string FullOpeningTag { get; set; }
            public int ClosingTagSize
            {
                get 
                {
                    return TagName.Length + 3;
                }
            }
        }
        #endregion
        
        /// <summary>
        /// splits the given xml file into smaller parts
        /// </summary>
        /// <param name="pathToXmlFile">the path to the xml file, that is to be splitted</param>
        /// <param name="maxSizeInBytes">app. maximum size of the resulting parts</param>
        /// <param name="resultPath">where to write (path) the resulting files</param>
        /// <returns>list of filepaths, that were created</returns>
        public static List<string> Split(string pathToXmlFile, int maxSizeInBytes, string resultPath=null)
        {
            if (resultPath == null)
                resultPath=System.IO.Path.GetDirectoryName(pathToXmlFile);
            List<string> res = new List<string>();
            // initialize variables
            int currentFileNumber = 1;
            int sizeOfClosingTags = 0;
            string fileWithoutExtension = System.IO.Path.Combine(
                resultPath,               
                System.IO.Path.GetFileNameWithoutExtension(pathToXmlFile));
            StringBuilder output = new StringBuilder();
            Stack<XmlOpeningTag> _openElements = new Stack<XmlOpeningTag>();

            // open the xml file within a xmlreader
            System.Xml.XmlTextReader xmlReader = new System.Xml.XmlTextReader(pathToXmlFile);
            // read from the file, element by element
            while (xmlReader.Read())
            {
                string currentXmlLine = string.Empty;
                XmlOpeningTag newOpeningTag = null;
                switch (xmlReader.NodeType)
                {
                    case XmlNodeType.Element: //its an xml tag element (opening)
                        string xmlElementName = xmlReader.Name;
                        // start with the tag
                        currentXmlLine = "<" + xmlElementName;
                        bool isEmptyElement = xmlReader.IsEmptyElement;
                        // add all attributes
                        while (xmlReader.MoveToNextAttribute()) 
                            currentXmlLine += " " + xmlReader.Name + "='" + xmlReader.Value + "'";
                        // if the element is empty, close it in line
                        if (isEmptyElement )
                            currentXmlLine += "/>";
                        //otherwise just close the tag
                        else
                        {
                            currentXmlLine += ">";
                            newOpeningTag =
                             new XmlOpeningTag(
                                    xmlElementName,
                                    currentXmlLine
                                    );
                            sizeOfClosingTags += newOpeningTag.ClosingTagSize;
                        }
                        break;
                    case XmlNodeType.Text: // text node
                        currentXmlLine = xmlReader.Value;
                        break;
                    case XmlNodeType.EndElement: //end of element tag
                       // pop the last opening tag
                       XmlOpeningTag openingTag = _openElements.Pop();
                       sizeOfClosingTags -= openingTag.ClosingTagSize;
                       currentXmlLine = "</" + xmlReader.Name + ">";
                       break;
                }
                
                // lets see, if we can add this new line
                if (output.Length + sizeOfClosingTags + currentXmlLine.Length >= maxSizeInBytes
                    && xmlReader.NodeType != XmlNodeType.EndElement 
                    // endelements will be added anyways, we cant close the xml 
                    // before adding the endelement
                    )
                {
                    // na we cannot
                    // close all open tags
                    Stack<XmlOpeningTag> helperStack = new Stack<XmlOpeningTag>();
                    while (_openElements.Count > 0)
                    {
                        XmlOpeningTag ol = _openElements.Pop();
                        output.Append("</" + ol.TagName + ">");
                        helperStack.Push(ol);
                    }
                    
                    string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                    res.Add(filePathToSave);
                    System.IO.File.WriteAllText(filePathToSave, output.ToString());
                    currentFileNumber++;
                    // clear the string builder, the old stuff was saved
                    output.Clear();
                    // open the "old open" tags in the new output
                    while (helperStack.Count > 0)
                    {
                        XmlOpeningTag ol = helperStack.Pop();
                        output.Append(ol.FullOpeningTag);
                        _openElements.Push(ol);
                    }

                }

                if (newOpeningTag != null)
                    _openElements.Push(newOpeningTag);
                output.Append(currentXmlLine);
            }
            if (output.Length > 0)
            {
                string filePathToSave = fileWithoutExtension + "." + currentFileNumber + ".xml";
                res.Add(filePathToSave);
                System.IO.File.WriteAllText(filePathToSave, output.ToString());
                currentFileNumber++;
            }
            
            xmlReader.Close();
            return res;
        }
    }

Montag, 8. Oktober 2012

WPF Diagramm zum Anzeigen der CPU Auslastung

Herausforderung

Ziel heute ist es ein eigenständiges UserControl in WPF zu schreiben welches "as is" in jede beliebige Alikation eingebunden werden. Aufgabe des UserControls ist das Anzeigen der Prozessorauslastung als Liniendiagramm.

1. Wie messe ich die aktuelle CPU Auslastung in %

Möglich wird das über den PerformanceCounter aus der Assembly System.Diagnostics
PerformanceCounter cpuCounter = new PerformanceCounter();
cpuCounter.CategoryName = "Processor";
cpuCounter.CounterName = "% Processor Time";
cpuCounter.InstanceName = "_Total";

Console.WriteLine("CPU Auslastung: {0} %", cpuCounter.NextValue());

Das war ja schonmal einfach... Jetzt fehlt nurnoch das Diagramm.

2. Liniendiagramm im XAML

Wie der Name schon sagt, besteht ein Liniendiagramm aus Linien, d.h wir benötigen ein ItemsControl, dass an eine Liste von Start-End-Punkten gebunden wird.
<ItemsControl ItemsSource="{Binding CPUUsagePoints}">
 <ItemsControl.ItemTemplate>
  <DataTemplate>
   <Canvas>
    <Line 
          X1="{Binding Start.X}" Y1="{Binding Start.Y}" 
          X2="{Binding End.X}" Y2="{Binding End.Y}" 
          Stroke="Green" StrokeThickness="0.3"
          >
    </Line>
   </Canvas>
  </DataTemplate>
 </ItemsControl.ItemTemplate>
</ItemsControl>
Da der C# System.Drawing.Point kein Referenzdatentyp, sondern ein Verbunddatentyp ist, benötigen wir eine analoge Klasse, und dazu noch eine Klasse für eine Strecke mit Start-Punkt und End-Punkt. Die Implementierung spar ich mir hier, und zeige lieber das aus dem ViewModel gebundene Property CPUUsagePoints.
public List<Point> _pointList = new List<Point>();
public List<Line> _diagramLines;
/// <summary>
/// diagram lines for binding
/// </summary>
public List<Line> DiagramLines
{
  get
  {
    if (_diagramLines == null)
    {
     _diagramLines = new List<Line>();
     InitDiagramm();
    }
  return _diagramLines;
  }
}

/// <summary>
/// initializes the diagram with f(x)=0
/// and connects the points with lines
/// </summary>
void InitDiagramm()
{
  int granularity = 2;
  for (int i = 0; i < (int)(_parent.RenderSize.Width / granularity); i++)
  {
    Line sep = new Line();
    if (_diagramLines.Count == 0)
    {
      sep.Start = new Point(i * granularity, _parent.RenderSize.Height - 1);
      _pointList.Add(sep.Start);
    }
    else
    {
      sep.Start = _diagramLines.Last().End;
    }

    sep.End = new Point(i * granularity, _parent.RenderSize.Height - 1);
    _pointList.Add(sep.End);

    _diagramLines.Add(sep);
  }
}

Wie man leicht sieht werden die Punkte so initialisiert, dass diese bei einem kartesischen Koordinatensystem die Funktion f(x)=0 abbilden, also eine klassische Nulllinie. Da wir in der Computergrafik ein anderes Koordinatensystem verwenden, müssen wir das Ganze die Y-Achse betreffend noch etwas umrechnen.

So weit, so gut - aber wie sollen wir nun die neuen Werte in das Diagramm "rein" bekommen, dafür gibt es bestimmt einige Möglichkeiten - ich habe mich dafür entschieden, die neuen Werte rechts anzuhängen und die bestehenden Punkte nach links zu verschieben. Abstrakt gesehen, brauche ich hier also 2 Funktionen:
  1. Füge_neuen_Wert_hinzu (prozent)
  2. Schiebe_alle_bisherigen_Punkte_nach_links()
Nicht ganz so abstrakt, aber mindestens genauso einfach wird die Implementierung:
/// <summary>
/// add a new value for the cpu usage
/// </summary>
/// <param name="percent">value in per cent</param>
void AddCpuUsage(int percent)
{
  try
  {
    percent = 100 - percent;
    lock (_pointList)
    {
      ShiftLeft(1);
      Point p = _pointList[_pointList.Count - 1];
      double valueToAchieve = ((float)percent / 100.0) * _parent.RenderSize.Height;
      p.Y = valueToAchieve;
    }
  }
  catch { }
 }

/// <summary>
/// shifts the diagram points left
/// </summary>
/// <param name="offset">the offset, how far to shift</param>
void ShiftLeft(int offset = 1)
{
  for (int i = 0; i < _pointList.Count - offset; i++)
  {
    Point p = _pointList[i];
    Point nextPoint = _pointList[i + offset];
    p.Y = nextPoint.Y;
  }
}
Wie man sieht, habe ich hier etwas geschummelt, ich verschiebe nicht die Punkte und hänge auch nichts hinten an, sondern shifte nur die Y-Werte nach links, und verändere dann den Y-Wert des letzten Punkts. Dadurch habe ich einen entscheidenden Vorteil - ich muss zum einen keine X-Verschiebung der einzelnen Punkte durchführen und zum Anderen kann ich die Liste DiagramLines so wie sie ist beibehalten, das Binding wird automatisch durch ein OnPropertyChanged in der Klasse Point bewerkstelligt.

Und für alle, die das Ganze nochmal praktisch nachvollziehen wollen, hier der Link zur Solution:
Solution CPUUsageVisualizer

Dienstag, 2. Oktober 2012

Event bei der Erstellung von neuen Fenstern

In komplexen Anwendungen ist es manchmal notwendig herauszufinden, wann eine MessageBox erscheint oder ein neues Fenster im aktuellen Prozess erzeugt wird. Oder gar, wann in einem anderen Prozess ein Fenster geöffnet wurde.

Das .Net Framework bringt von Hause aus leider kein solches Event mit, daher müssen wir hier etwas basteln. Die Windows API hat hier alles, was wir brauchen (nähere Infos zu unmanaged APIs unter http://www.pinvoke.net ) Um genau zu sein sind die Funktionen, die wir benötigen in der user32.dll.

EnumDesktopWindows

static private delegate bool EnumWindowsDelegate(IntPtr hWnd, int lParam);

[DllImport("user32.dll")]
static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsDelegate lpfn, IntPtr lParam);

Diese Funktion iteriert durch alle Fenster, die dem Fenster mit dem handle hDesktop untergeordnet sind. Für jede Iteration wird der delegate lpfn aufgerufen. Dadurch können wir uns zu jedem Zeitpunkt eine Liste mit den aktuell offenen WindowHandles aufbauen:
static List<intptr> _currentWindows = new List<intptr>();
IntPtr hwndDesktop = IntPtr.Zero; // current desktop
bool success = EnumDesktopWindows(hwndDesktop, 
                new EnumWindowsDelegate(
                 (hWnd, lparam) => 
                 { 
                   currentWindows.Add(hWnd); 
                   return true; 
                 }), IntPtr.Zero);

GetWindowThreadProcessId

Da wir nicht zwingend alle neu erstellten Fenster melden möchten, sondern wahlweise auch nur die Fenster, die zum aktuellen Prozess gehören,  wird die ProzessID des WindowHandle benötigt. Das können wir folgendermaßen auflösen:
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
// get the process id of a window handle
uint lpdwProcessId;
GetWindowThreadProcessId(winHndl, out lpdwProcessId);

GetWindowText

Zu guter Letzt sollten wir neben dem WindowHandle vielleicht noch den Text des Fensters herausfinden, denn mit dem Handle kann man vermutlich im verwalteten Code nicht viel anfangen.

[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
// get the window title
StringBuilder windowName = new StringBuilder(256);
GetWindowText(hWnd, windowName, windowName.Capacity);

Alle Drei zusammen

Der Ablauf des Programms sollte nun eigentlich relativ klar sein (hier ein kurzer Abschnitt des essentiellen Ablaufs als Pseudocode):
erzeuge neuen Thread mit folgendem Code:
Liste bekannteFenster := leere Liste
Liste aktuelleFenster := leere Liste
do
  • aktuelleFenster <- EnumDesktopWindows
  • für alle f in aktuelleFenster , aber nicht in bekannteFenster
    • GetWindowThreadProcessId ?= CurrentProcess.Id
    • -> Raise WindowCreatedEvent(f, GetWindowText(f))
while (true)

Und für alle, die es gleich mal ausprobieren wollen, hier mein Implementierungsvorschlag:

static class ChildWindowOpenHook
{
  #region private fields / delegates
  private delegate bool EnumWindowsDelegate(IntPtr hWnd, int lParam);
  static private Thread _workerThread = null;
  static List _currentWindows = new List();
  static List _knownWindows = new List();
  static bool _isRunning = true;
  static bool _onlyOwnWindows = false;
  #endregion 

  #region DLLImport
  [DllImport("user32.dll", SetLastError = true)]
  static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsDelegate lpfn, IntPtr lParam);
  
  [DllImport("user32.dll", SetLastError = true)]
  static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
  
  [DllImport("user32.dll")]
  private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
  #endregion

  #region Starter
  /// 
  /// C'tor
  /// starts the enumeration thread
  /// 
  public static void Start(bool onlyOwnWindows)
  {
   _onlyOwnWindows = onlyOwnWindows;
   // 1. start enumerating the first time
   EnumerateWindows();
   // 2. copy those current windows in out old window list
   _knownWindows.AddRange(_currentWindows);
   // 3. start the enumeration thread, which will enumerate through the
   // current windows periodically
   ThreadStart tStart = new ThreadStart(ThreadRunner);
   _workerThread = new Thread(tStart);
   _workerThread.Name = "window watcher thread";
   _workerThread.Priority = ThreadPriority.BelowNormal;
   _workerThread.IsBackground = true; // important, so the mainthread will not wait on this one

   _workerThread.Start();
  }
  #endregion

  #region ThreadRunner
  /// 
  /// Thread Runner 
  ///  - iterates through all desktop windows
  ///  - fires new window event, when a new window has been detected
  /// 
  static private void ThreadRunner()
  {
   try
   {
    while (_isRunning)
    {
     // clear the current window list
     _currentWindows.Clear();
     // populate the currentwindow list
     EnumerateWindows();
     // find out which windows are not known already
     foreach (IntPtr winHndl in _currentWindows.Where(hndl => !_knownWindows.Contains(hndl)))
     {
      // add the windowhandle of a "NEW WINDOW" to our list of known windows
      _knownWindows.Add(winHndl);
      
      // get the process id of that new window
      uint lpdwProcessId;
      GetWindowThreadProcessId(winHndl, out lpdwProcessId);
      // find out if its on the same process, as we are
      if (
       !_onlyOwnWindows
       || (uint)System.Diagnostics.Process.GetCurrentProcess().Id == lpdwProcessId)
      {
       // raise the event
       OnWindowCreated(winHndl);
      }

     }
     Thread.Sleep(500);
    }


   }
   catch (Exception aException)
   {
    // write down the exeption that occured
    Console.WriteLine("exception in thread:" + aException);
   }
  }
  #endregion

  #region EnumerateWindows
  static private void EnumerateWindows()
  {
   IntPtr hwndDesktop = IntPtr.Zero; // current desktop
   bool success = EnumDesktopWindows(hwndDesktop, 
    new EnumWindowsDelegate(
     (hWnd, lparam) => 
     { 
      _currentWindows.Add(hWnd); 
      return true; 
     }), IntPtr.Zero);

   // if we have no success, lets throw an error
   if (!success)
   {
    // get the last win32 error code
    int errorCode = Marshal.GetLastWin32Error();
    string errorMessage = string.Format("EnumDesktopWindows failed, error code: {0}.", errorCode);
    throw new Exception(errorMessage);
   }
  }
  #endregion

  #region Stopper
  /// 
  /// Stops the ChildWindowOpenHook
  /// 
  public static void Stop()
  {
   _isRunning = false;
   _workerThread.Abort();
  }
  #endregion

  #region Event Handling
  /// 
  /// delegate for WindowCreated event
  /// 
  /// name of the window, that has been created
  /// window handle
  public delegate void WindowCreatedEventHandler(string windowName, IntPtr hWnd);
  
  /// 
  /// handles window created events
  /// 
  public static event WindowCreatedEventHandler WindowCreated;

  /// 
  /// being called, when a window has been created
  /// 
  /// the window handle
  private static void OnWindowCreated(IntPtr hWnd)
  {
   if (WindowCreated!=null && _isRunning)
   { 
    // get the window title
    StringBuilder windowName = new StringBuilder(256);
    GetWindowText(hWnd, windowName, windowName.Capacity);
    // handle the event
    WindowCreated(windowName.ToString(), hWnd);
   }
  }
  #endregion
}

Man kann sich folgendermaßen an das event hängen:
public MainWindow()
{
 InitializeComponent();
 ChildWindowOpenHook.Start(true);
 ChildWindowOpenHook.WindowCreated += ChildWindowOpenHook_WindowCreated;
}

void ChildWindowOpenHook_WindowCreated(string windowName, IntPtr hWnd)
{
 Console.WriteLine(windowName);
}

Sonntag, 30. September 2012

Beliebiges WPF-Control auf anderen Thread auslagern

In manchen Situationen ist es notwendig, dass man einzelne GUI Elemente vom Hauptthread entkoppelt und auf einen eigenen Thread aulagert.
Wichtig! Bevor man diesen Ansatz jedoch verfolgt, sollte man versuchen die "Arbeit", die hier den Hauptthread belastet, auszulagern.
Bei meiner Untersuchung zu dem Thema bin ich auf einen Post von Dwayne Need gestoßen, der die Probleme und einen Lösungsansatz sehr gut erklären: Multithreaded UI: HostVisual

Damit wäre eigentlich schon alles gesagt..., aber: muss ich wirklich für jedes UI-Element, dass ich auslagern will so viel Code schreiben, bzw. muss ich immer den gleichen Code (Erzeugen des Threads und Kaskadieren der Visual Elemente) duplizieren, wenn ich ein MediaElement und eine Progressbar in verschiedenen Threads laufen lassen will? Vielleicht möchte man auch mal mehrere Elemente zusammen in einem Thread laufen lassen. - Mit anderen Worten: Ich will flexibler sein.

Meine Vorstellung ist also, ein Frameworkelement zu haben, das, egal was ich einbette, dies auf einem anderen Thread laufen lässt:
<VisualWrapper>
   <Progressbar Value="{ProgressValue}" />
</VisualWrapper>

Und jetzt die Enttäuschung, für alle die bis hier gelesen haben: Eine saubere Lösung existiert meines Wissens dafür nicht... (jetzt wird's schmutzig),denn an und für sich wird der VisualTree rekursiv aufgebaut, d.h. der Content eines jeden Elements wird initialisiert, bevor dieser als Child an den Parent gehängt wird. Da wir diesen Mechanismus nicht kontrollieren können, müssen wir einen Weg finden, den Content zu übergeben, ohne diesen zu Initialisieren. Abhilfe schafft hier der XamlReader, dieser ermöglich es uns während der Programmlaufzet ein beliebiges Xaml zu instanziieren. Das Xaml übergeben wir dem Control dann als CDATA über ein Property:
<VisualWrapper>
  <VisualWrapper.Content>
    <![CDATA[
     <ProgressBar 
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      Height="20" Width="100" Value="{Binding ProgressValue}" />
    ]]>
  </VisualWrapper.Content>
</VisualWrapper>
Im VisualWrapper erfolgt dann die Initialisierung in einem eigenen Thread über den XamlReader:
 /// 
 /// Der Content als XAML-String, die Initialisierung erfolgt Postum
 /// 
 public string Content
 {
  set
  {
   CreateThreadedChild(value);
  }
 }

 /// 
 /// erstellt das Child-Element (aus dem Content-String) auf einem seperaten Thread
 /// 
 /// 
 void CreateThreadedChild(string xaml)
 {
  // erstelle einen Thread für das Content-Element
  Thread thread = new Thread(new ParameterizedThreadStart(ContentWorkerThread));
  thread.SetApartmentState(ApartmentState.STA);
  thread.Name = "GUI Thread";
  thread.IsBackground = true;
  thread.Start(xaml);

  // warte bis der Thread signalisiert, dass er fertig ist mit der Initialisierung
  s_event.WaitOne();
 }

 

 /// 
 /// XAML in WPF-Element umwandeln und auf die Oberfläche verlinken
 /// 
 /// XAML-String des Content
 void ContentWorkerThread(object arg)
 {
  // erstelle das WPF-Element aus dem übergebenen XAML-String
  StringReader stringReader = new StringReader(arg.ToString());
  XmlReader xmlReader = XmlReader.Create(stringReader);
  object content = XamlReader.Load(xmlReader);


  // nun wird die VisualTargetPresentationSource erzeugt (mit dem hier enthaltenen HostVisual als parent)
  VisualTargetPresentationSource visualTargetPS = new VisualTargetPresentationSource(_child);
  // der MainThread kann nun weiter arbeiten
  s_event.Set();

  // nun wird der DataContext von der obersten Ebene auf das aktuelle Element geschleift
  // -> per Invoke, da beide Elemente auf verschiedenen Threads liegen
  // (alternativ kann man den DataContext übergeben, bzw. sich auch an das entsprechende event hängen)
  object parentContext = null;
  
  
  this.Dispatcher.Invoke(new Action(() => { parentContext = this.DataContext; }));
  // die PresentationSource bekommt nun den DataContext 
  visualTargetPS.DataContext = parentContext;


  // hier wird nun der oberste Knoten im VisualTree gesetzt (unser Content)
  visualTargetPS.RootVisual = (Visual)content;



  // jetzt wird die (unendliche) Prozessschleife auf dem aktuellen Thread gestartet 
  System.Windows.Threading.Dispatcher.Run();
 }
Ich habe das ganze noch mit dem Code von Dwayne als Projekt zusammengefasst:
Visual Studio Solution: TestGenericThreadedContainer

Freitag, 28. September 2012

Code Snippet: Thread sicherer Zugriff auf WPF-Control

Motivation

Verwendet man in WPF konsequent das MVVM-Modell, so greift man immer auf das ViewModel zu, anstatt direkt mit irgendwelchen GUI-Elementen zu interagieren. Da der Zugriff auf das ViewModel wiederum threadunabhängig ist, kommt man eigentlich nie (oder eher selten) in die Lage folgenden Code einzusetzen. Aber wie immer bestätigen Ausnahmen die Regel - mir fällt zwar spontan kein einfacher Anwendungsfall ein, aber in der täglichen Praxis habe ich diesen Code schon einige male verwendet:

// nehmen wir an, wir haben eine TextBox und wollen threadsicher den Text verändern
 class ThreadSafeTextBox : TextBox
    {
        public void SetTextThreadSafe(string text)
        {
                // prüft, ob der aktuelle Thread dem Dispatcher 
                // dieses Controls zugeordnet ist
                if (this.Dispatcher.CheckAccess())
                {
                    // Ist dies der Fall, können wir den Text einfach setzen.
                    // Man sollte beachten, dass man sich jetzt in dem 
                    // Dispatcher Thread befindet, man sollte hier also nur 
                    // "schnell" auf die GUI zugreifen. Alles was lange dauert
                    // und nichts direkt mit der GUI zu un hat, gehört hier nicht rein!
                    base.Text = text;
                }
                else
                {
                    // anderenfalls müssen wir den Dispatcher "invoken"
                    this.Dispatcher.Invoke(new Action(
                        () =>
                        {
                            // jetzt können wir sicher auf den Text zugreifen
                            SetTextThreadSafe(text);
                        })
                        );
                }
            }
        }
    }

Donnerstag, 27. September 2012

Code Snippet: XAML Progress Indicator (Marquee)

Motivation

In Windows 95 haben wir gemerkt, dass Fortschrittsbalken nicht nur wachsen, sondern auch schrumpfen können. Progressive Fortschrittskorrektur nennt das der Klugscheißer, sogar die Überschreitung der 100% war in der Vergangenheit durchaus möglich.

Für den Anwender sind Fortschrittsbalken hauptsächlich ein Indikator dafür, dass ein Vorgang noch sehr lange dauert - Wenn der Balken nicht zuverlässig ist, sollte man ihn ganz lassen, oder eben nur durch "Bewegung" dem Anwender signalisieren, dass etwas bearbeitet wird. Bei diversen Videoplattformen kennt man ddies als "den Kreisel des Wartens". In Windows 8 jedoch sieht man oft eine Schlange von Punkten die von links nach rechts Pendeln, diese habe ich hier in XAML nachempfunden, viel Spaß damit:
 

<Canvas VerticalAlignment="Center" HorizontalAlignment="Center">
 <Path Fill="Black" Name="Path5">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell5"></EllipseGeometry>
  </Path.Data>
 </Path>

 <Path Fill="Black" Name="Path4">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell4"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path3">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell3"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path2">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell2"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path1">
  <Path.Data >
   <GeometryGroup>
    <EllipseGeometry Center="15,0" RadiusX="3.5" RadiusY="3.5" x:Name="Ell1"></EllipseGeometry>
   </GeometryGroup>

  </Path.Data>
  <Path.Triggers>
   <EventTrigger RoutedEvent="Path.Loaded">
    <BeginStoryboard>
     <Storyboard SpeedRatio="4">
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell1" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:2" Value="32,0" />
       <EasingPointKeyFrame KeyTime="0:0:8" Value="42,0" />
       <EasingPointKeyFrame KeyTime="0:0:10" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell2" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:1" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:3" Value="15,0" />
       <EasingPointKeyFrame KeyTime="0:0:9" Value="25,0" />
       <EasingPointKeyFrame KeyTime="0:0:11" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell3" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:2" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:4" Value="0,0" />
       <EasingPointKeyFrame KeyTime="0:0:10" Value="10,0" />
       <EasingPointKeyFrame KeyTime="0:0:12" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell4" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:3" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:5" Value="-15,0" />
       <EasingPointKeyFrame KeyTime="0:0:11" Value="-5,0" />
       <EasingPointKeyFrame KeyTime="0:0:13" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell5" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:4" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:6" Value="-30,0" />
       <EasingPointKeyFrame KeyTime="0:0:12" Value="-20,0" />
       <EasingPointKeyFrame KeyTime="0:0:14" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>

      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path1" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:2" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:8" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:10" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path2" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:3" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:9" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:11" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path3" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:2" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:4" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:10" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:12" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path4" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:5" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:11" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:13" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path5" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:4" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:6" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:12" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:14" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
     </Storyboard>
    </BeginStoryboard>
   </EventTrigger>
  </Path.Triggers>
 </Path>
</Canvas>

Dienstag, 19. Juni 2012

XAML: XPath Binding erweitern durch XPathHelper als MultiConverter

Vor kurzem habe ich eine eher komplexe Lösung implementiert, bei der aus technischen Gründen ein XAML komplett an ein XML gebunden wird. (nicht ganz, es gibt ein ViewModel für das generische Piping der Button Commands) An sich bietet WPF hier durch die Verwendung von XPath-Ausdrücken direkt im XAML eine sehr gute Möglichkeit, und für viele Aufgaben ist dies auch ausreichend.

Jedoch bin ich dabei über ein etwas komplexeres Problem gestolpert, denn nicht jeder XPath-Ausdruck kann zu einem XmlNode evaluiert werden, was dann zur Laufzeit folgenden Fehler hervorbringt:



System.Windows.Data Error: 45 : XML binding failed. Cannot obtain result node collection because of bad source node or bad Path.; SourceNode='#document'; Path='count(/ROOT/Adresse)' BindingExpression:Path=/; DataItem='XmlDocument' (HashCode=9035653); target element is 'Label' (Name=''); target property is 'Content' (type 'Object') XPathException:'System.Xml.XPath.XPathException: Der Ausdruck muss in einem Knotensatz resultieren.
bei System.Xml.XPath.XPathNavigator.Select(XPathExpression expr)
bei System.Xml.XmlNode.SelectNodes(String xpath)
bei MS.Internal.Data.XmlBindingWorker.SelectNodes()'

Beispiel: Eine Adress-Verwaltung im XML

Kommen wir erstmal kurz zu einem Beispiel an dem ich vereinfacht die Herausforderung deutlich machen möchte. Angenommen unser XML besteht aus einer Liste von Adressen - unser Programm bietet nun die Möglichkeit, Adressen hinzuzufügen, jedoch mit einer Einschränkung: Die gleiche Person darf nur 2 mal auftauchen (gleicher Vor- und Nachname). Um das ganze nun im Code nicht abfangen zu müssen, wollen wir den Button zum Speichern deaktivieren, wenn die besprochene Regel greift.
Das XML hat folgende Struktur:

   


Grundsätzlich besteht unser XAML aus einer ListBox.
       <ListBox
            HorizontalContentAlignment="Stretch"
            ItemsSource="{Binding XPath=/root/adresse}" DataContext="{Binding DataAsXmlDocument}" ItemContainerStyle="{StaticResource ContainerStyle}" >
Außerdem haben wir in den Window.Resources entsprechend für den unselektierten und den selektierten Zustand ein DataTemplate, dass über deinen Style-Trigger gesetzt wird. Im selektierten Zustand kann der gewählte Adresssatz bearbeitet werden und mit einem Button wird das Speichern bestätigt.
                <DataTemplate x:Key="ItemTemplate">
                    <Grid HorizontalAlignment="Stretch"  Width="Auto">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                        </Grid.RowDefinitions>
                        <Label Content="{Binding XPath=@vorname}"       Grid.Column="0" Grid.Row="0"/>
                        <Label Content="{Binding XPath=@name}"          Grid.Column="1" Grid.Row="0"/>

                        <Label Content="{Binding XPath=@strasse}"       Grid.Column="0" Grid.Row="1"/>
                        <Label Content="{Binding XPath=@hausnummer}"    Grid.Column="1" Grid.Row="1"/>

                        <Label Content="{Binding XPath=@plz}"           Grid.Column="0" Grid.Row="2"/>
                        <Label Content="{Binding XPath=@ort}"           Grid.Column="1" Grid.Row="2"/>
                    </Grid>
                </DataTemplate>

                <DataTemplate x:Key="SelectedItemTemplate">
                    <Grid HorizontalAlignment="Stretch"  Width="Auto">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                        </Grid.RowDefinitions>
                        <TextBox Text="{Binding XPath=@vorname}"       Grid.Column="0" Grid.Row="0"/>
                        <TextBox Text="{Binding XPath=@name}"          Grid.Column="1" Grid.Row="0"/>

                        <TextBox Text="{Binding XPath=@strasse}"       Grid.Column="0" Grid.Row="1"/>
                        <TextBox Text="{Binding XPath=@hausnummer}"    Grid.Column="1" Grid.Row="1"/>

                        <TextBox Text="{Binding XPath=@plz}"           Grid.Column="0" Grid.Row="2"/>
                        <TextBox Text="{Binding XPath=@ort}"           Grid.Column="1" Grid.Row="2"/>
                        
                        <Button Content="Speichern" Grid.Column="1" Grid.Row="3"/>
                    </Grid>
                </DataTemplate>

                <Style TargetType="{x:Type ListBoxItem}" x:Key="ContainerStyle">
                    <Setter Property="ContentTemplate" Value="{StaticResource ItemTemplate}" />
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="ContentTemplate" Value="{StaticResource SelectedItemTemplate}" />
                        </Trigger>
                    </Style.Triggers>
                </Style>

Das ganze sieht dann visualisiert (mit gebundenem Beispiel-XML) sieht so aus.
Nicht schön, aber ich bin ja auch kein Designer ;-)

Unabhängig davon, wie diese Adress-Verwaltung neue Einträge hinzufügt, oder Einträge speichert, liegt die Herausforderung nun darin, den Speichern-Button auszublenden, wenn bereits genau 2 andere Personen existieren, die den gleichen Vor- und Nachnamen haben.


Ansatz 1: Die Konservative Lösung (code behind)

Naja, der naive Ansatz wäre jetzt die Logik für diese Regel einfach im code behind zu realisieren.
Dazu müssen im XAML einige Anpassungen gemacht werden. Zum einen müssen wir die zugehörigen Textboxen an das TextChangedEvent hängen und dort dann auf unsere ListBox zugreifen, dh. wir müssen diese über den Namen refernzieren können. Um den Speichern-Button zu deaktivieren, gilt das gleiche.
<!-- [..] -->
<TextBox Text="{Binding XPath=@vorname,UpdateSourceTrigger=PropertyChanged}"       
   Grid.Column="0" Grid.Row="0"  
   TextChanged="TextBox_TextChanged" />
<TextBox Text="{Binding XPath=@name,UpdateSourceTrigger=PropertyChanged}"          
   Grid.Column="1" Grid.Row="0"  
   TextChanged="TextBox_TextChanged" />
<!-- [..] -->
<Button Content="Speichern" 
           Grid.Column="1" Grid.Row="3" 
           Name="Button_Speichern"/>

<!-- [..] -->
        <ListBox
            Name="lstbAdressen"
            HorizontalAlignment="Stretch"
            HorizontalContentAlignment="Stretch"
            ItemsSource="{Binding XPath=/root/adresse}" 
   DataContext="{Binding DataAsXmlDocument}" 
   ItemContainerStyle="{StaticResource ContainerStyle}" >
<!-- [..] -->

Jetzt fehlt uns nurnoch der code behind:
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
 XmlNode selectedItem = (XmlNode)lstbAdressen.SelectedItem;
 if (selectedItem != null)
 { 
  XmlDocument adressenXml = lstbAdressen.DataContext as XmlDocument;
  if (adressenXml != null)
  {
    XmlNodeList sameNameNodes = 
        adressenXml.SelectNodes(
                 "/root/adresse[@vorname = '" 
                 + selectedItem.Attributes["vorname"].Value 
                 + "' and @name = '" 
                 + selectedItem.Attributes["name"].Value 
                 + "']");
                  
    for (
      int i = 0; 
      i < VisualTreeHelper.GetChildrenCount((sender as FrameworkElement).Parent); 
      i++)
    {
     DependencyObject child = 
         VisualTreeHelper.GetChild((sender as FrameworkElement).Parent, i);
     if (child != null 
         && child is FrameworkElement 
         && ((FrameworkElement)child).Name == "Button_Speichern")
     {
       if (sameNameNodes.Count == 3)
       {
        ((FrameworkElement)child).IsEnabled = false;
       }
       else
       {
        ((FrameworkElement)child).IsEnabled = true;
       }

     }
    }
   }
  }
}


Wie man sieht ist diese Lösung mit den ca. 10 Codezeilen nicht ganz so trivial wie angenommen. Prinzipiell liegt ein größerer Aufwand darin, den Speichern-Button zu finden, da dieser in einem DataTemplate gekapselt ist. Nichtsdestotrotz funktioniert dieser Ansatz ganz gut, vielleicht gibt performancemäßige Probleme, aber bei kleinen Datensätzen sollte es ganz gut klappen.

Ansatz 2: MVVM-Pattern

Die Lösung mit dem code behind wird bei vielen WPF-Entwicklern arge Bauchschmerzen verursachen, da es ein datentechnisches Problem über visuelle Events und damit verbunden direkten Zugriff auf die View verursacht als Lösung heranzieht. Sauberer ist natürlich das ganze über ein Command-Property im ViewModel zu lösen. Wir sparen uns dadurch die Iteration durch den VisualTree und können komplett von der View abstrahieren.

Ansatz 3: Die Lösung im XAML

Eine Lösung komplett im XAML ist vermutlich nicht möglich, denn XPath kann keine Variablen speichern. Um durch alle Adress-Knoten zu iterieren und nach einem Attribut aus dem aktuellen Kontext zu vergleichen ist dies unabdingbar. Es müssste etwas geben um im XPath Platzhalter zu definieren, um diese wiederum dann durch weitere Bindings zu ersetzen, so etwas wie:

{Binding XPath=/root/adresse[@vorname={Binding XPath=@vorname} and @name={Binding XPath=@name}] }

Abgesehen davon, dass das vollkommener Quatsch ist, kommt noch hinzu, dass wir die Anzahl der Nodes zählen müssten:
{Binding XPath=count(/root/adresse[@vorname={Binding XPath=@vorname} and @name={Binding XPath=@name}]) }

Ich hab lange überlegt wie man auf elegante Weise dieses Problem lösen kann, mit einer MarkupExtension ? Einem Converter? Oder doch was ganz anderem?
Die meines Erachtens beste Lösung ist die Verwendung eines MultiBinding. Die Idee dahinter ist, die Funktion String.Format mit einem SelectNodes zu kombinieren.
Das MultiBinding an sich soll folgendermaßen funktionieren:

<MultiBinding Converter="{StaticResource XPathHelper}" 
ConverterParameter="count:/root/adresse[@vorname={0} and @name={1}]"> 
  <Binding XPath="@vorname" />
  <Binding XPath="@name" />
  <Binding XPath="." />
</MultiBinding>

Prinzipiell sollte jetzt jedem klar sein, wie der Converter implementiert wird, für alle die gern kopieren & pasten, hier ein Implementierungsvorschlag:
    public class XPathHelper : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            string xpath = (string)parameter;
            string.Format(xpath, values);
            bool justCountResult = false;
            if (xpath.StartsWith("count:", StringComparison.InvariantCultureIgnoreCase))
            {
                xpath = xpath.Substring(6);
                justCountResult = true;
            }

            XmlDocument doc = (XmlDocument) values[values.Count() - 1];
            if (doc != null)
            {
                XmlNodeList nodesFound = doc.SelectNodes(xpath);
                if (justCountResult) // return count only
                    return nodesFound.Count.ToString();
                if (nodesFound.Count > 0) //return only the value of the first node though
                    return nodesFound[0].Value;
            }
            return null; // nothing found or no xmldocument given
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }