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();
        }
    }

Montag, 11. Juni 2012

Dictionary oder Hashtable

Motivation

In einigen Programmiersprachen ist der Einsatz von Hashtabellen (oder auch assoziativen Arrays) Gang und Gäbe. So zum Beispiel in Perl oder PHP. In C# existiert diese Datenstruktur selbstverständlich auch, jedoch existiert eine generisch typisierte Alternative zu den "normalen" Hashtabellen, das Dictionary.
Doch welche dieser beiden Implementierungen ist vorzuziehen?
Die msdn-Dokumentation (msdn: Hashtable-Auflistungstyp und Dictionary-Auflistungstyp) gibt hierzu leider nur wenig Auskunft, lediglich bei Werttypen soll ein Dictionary leistungsfähiger sein. Der Artikel legt die Vermutung nahe, dass bis auf die Typisierung und ein wenig syntaktischer Zucker (wie TryGetValue) beide Implementierungen ziemlich identisch sind.


Was ist eigentlich eine Hashtabelle?

Eine Hashtabelle wird oft mit Eimern (Buckets) verglichen. Da ich viel bei einem schwedischen Möbelhaus einkaufe, würde ich eher einen Vergleich mit dem Ikea-Lager vorziehen. Grundsätzlich besteht eine Hashtabelle immer aus Schlüssel-Wert-Paaren, anhand eines Schlüssels kann man in einer Hashtabelle ziemlich schnell (konstante Zeit) einen Wert nachschlagen.

Kommen wir zu dem Vergleich mit dem Möbellager, hier wäre der Schlüssel das aufgebaute Möbel, so wie wir es in der Möbelausstellung sehen. Der Wert, den wir jedoch im Möbellager erhalten wollen (oder sollen), ist der zugehörige Bausatz. Im Möbellager müssen wir nun, nachdem wir uns für ein bestimmtes Möbel entschieden haben, den zugehörigen Bausatz suchen, sondern verwenden eine Lagerplatznummer. Dadurch wird jedem Möbel ein eindeutiger Lagerplatz zugewiesen. In der Informatik ist diese Abbildung die Hashfunktion, bei Ikea wird es dafür keine mathematische Formel geben, sondern vielmehr ein Fakturierungsprogramm, dass die Abbildung durchführt.
                    f: Möbel -> Lagerplatz

Testparameter

Um diesen Test durchzuführen, habe ich ein kleines Programm geschrieben, welches Keys und Values jeweils als Strings variabler Länge erzeugt (zwischen 10 und 100 Zeichen). Bei der Suche betrachte ich 2 Szenarien, zum einen die Option, dass jeder Suchschlüssel auch in der Hashtable vorkommt, und zum Anderen, die Option, die wohl am Häufigsten in der Paxis auftaucht: Es existieren etwa 50% der Schlüssel, nach denen gesucht wird.

Test 1: Einfügen von Schlüssel-Wert-Paaren

Beim Einfügen gibt es einen klaren Sieger: das Dictionary, bei großen Datensätzen (ca. 1.000.000) ergibt sich ein Vorteil von ca. 35 %, bei kleineren Tabellen, soger ein Vorteil von bis zu 50 %.

Test 2: Suchen von Schlüsseln

Hier ist das Ergebnis nicht ganz klar, bei großen Datensätzen gewinnt hier die Hashtable mit knapp 6 %. Bei kleinen Testmengen liegt der Vorteil jedoch bei dem Dictionary (ca. 8 %)

Während sich die Nachschlagezeiten in der Hashtable nach vorhandenen und nicht vorhandenen Schlüsseln die Waage hält, ist das Dictionary sogar noch um ca. 3 % schneller, wenn die Hälfte der Schlüssel nicht im Dictionary enthalten sind.

Tipp

Bei der Verwendung des Dictionary ist mit aufgefallen, dass es nur einen Vorteil gibt, wenn man anstatt des vorherigen Prüfens (mittels ContainsKey) mit der Funktion TryGetValue arbeitet:
if (testDictionary.ContainsKey(keyToFind))
    valueFound = testDictionary[keyToFind];

// Besser:

string valueFound = string.Empty;
testDictionary.TryGetValue(keyToFind, out valueFound);


Fazit

Nimmt man die Testergebnisse her, so überwiegt der <u>Vorteil bei dem Dictionary</u>. Dem zugegebener Maßen geringen Performance-Vorteile der Hashtable beim Suchen in großen Datensätzen, steht ein eindeutig besserer Programmierstil, durch die Verwendung von Dictionaries, gegenüber.<br /> <br /></body>

Samstag, 9. Juni 2012

VisualStudio Performance Wizard mit signierten Assemblies

Grundsätzlich kann es beim Verwenden des VisualStudio Profiler zu Problemen kommen, wenn man mit signierten Assemblies arbeitet. Beim Abschalten der Signierung für die gerade zu überwachende Library kann es jedoch zu Problemen kommen, wenn die abhängigen, signierten Assemblies geladen werden. Daher empfiehlt es sich beim Profiling die Signatur komplett abzuschalten.


Abschalten der Assembly-Signierung im VisualStudio Profiler

Rechte MT auf die Performance-Instanz > Eigenschaften und dort als Post-Instrument Event folgendes angeben:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\sn.exe"  -Vr *