Gestern wurde ich gefragt ob es denn möglich sein, in einer WPF ListView Zeilennummern anzeigen zu lassen. Sicherlich ein Anwendungsfall, der nicht einmalig ist.
Meine Recherchen haben ergeben, dass es nicht implizit möglich ist Zeilennummern anzeigen zu lassen (also es gibt keine Property oder ähnliches dafür). Der erste Lösungsansatz ist also ein Converter, die ja bekanntlich alles in alles andere konvertieren können, warum also keine Berechnung der Zeilnummern?
Ein solcher Converter muss zwei Informationen erhalten:
1) Die Instanz der ListView, die ihn aufgerufen hat
2) Der Wert, der momentan an die ListView gebunden wird
Mit Hilfe dieser Informationen kann nun über die Items der ListView iteriert werden und ein Vergleich mit dem aktuellen Wert führt dann zur Position innerhalb der ListView. Versuche ein Binding mit einem Converter zu versehen und diesem als ConverterParameter das aufrufende Objekt mitzugeben schlugen leider fehl, stattdessen habe ich ein MultiBinding erzeugt und beide Informationen als separate Bindings übergeben. Der XAML Aussschnitt dazu sieht folgendermaßen aus:
1: <ListView x:Name="lsvOfferMoreLP"
2: Height="Auto"
3: IsSynchronizedWithCurrentItem="True"
4: AllowDrop="True"
5: SelectionChanged="lsvOfferMoreLP_SelectionChanged" TabIndex="28">
6: <ListView.Resources>
7: </ListView.Resources>
8: <ListView.View>
9: <GridView>
10: <GridViewColumn Header="LineNumber" >
11: <GridViewColumn.DisplayMemberBinding>
12: <MultiBinding Converter="{StaticResource ConverterLineNumbers}">
13: <Binding Path="."/>
14: <Binding>
15: <Binding.RelativeSource>
16: <RelativeSource Mode="FindAncestor" AncestorType="{x:Type ListView}" />
17: </Binding.RelativeSource>
18: </Binding>
19: </MultiBinding>
20: </GridViewColumn.DisplayMemberBinding>
21: </GridViewColumn>
22: <GridViewColumn DisplayMemberBinding="{Binding Path=.}" Header="Text" />
23: </GridView>
24: </ListView.View>
25: </ListView>
Auffallend ist die zweite Bindung, die an eine RelativeSource geht. Wie vielleicht aus der Syntax zu verstehen ist, bezieht sich dieses Binding auf den nächsten Vorfahren im Control Tree, der eine ListView ist. Damit enthält unsere Converter-Klasse (die das Interface IMultivalueConverter implementiert) sowohl die Instanz der auslösenden Listview sowie der aktuell zu bindende Wert.
In der Klasse ConverterLineNumbers, die als Ressource instanziiert und im MultBinding verwendet wird muss also das Interface IMultiValueConverter eingebunden werden, das u.a. folgende Convert-Methode vorschreibt:
1: public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
2: {
3: object itemValue = values[0];
4: var source = values[1] as ListView;
5: int count = -1;
6: if (itemValue != null && source != null)
7: {
8: count = 0;
9: foreach (var item in source.ItemsSource)
10: {
11: count++;
12: if (itemValue.Equals(item))
13: break;
14: }
15: }
16: return count.ToString();
17: }
In der Convert-Methode wird nun über jedes Element der ListView iteriert und wenn das gerade zu bindende Element gleich dem Element der ItemsSource ist wird die Iteration abgebrochen. Der Zähler, der mit jeder Iteration um eins hochgesetzt wird gibt also die Position des Elementes in der Listview wider. Diese Zahl (umgewandelt zu einem String) ist das Ergebnis der Convert Methode und wird in der ListView an entsprechender Stelle dargestellt.
Der Vergleich einString.Equals(einAndererString) liefert true zurück, wenn beide Strings den selben Inhalt haben, bei “normalen” Referenztypen sollte tatsächlich ein Vergleich auf Objektgleichheit durchgeführt werden!!
Das Projekt zum Downloaden und Ausprobieren findet sich hier:
http://cid-fd4d63530af59c99.skydrive.live.com/self.aspx/.Public/Projekte/WPF/LineNumbers.zip