Posts mit dem Label xaml werden angezeigt. Alle Posts anzeigen
Posts mit dem Label xaml werden angezeigt. Alle Posts anzeigen

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

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