I want to be able to convert an integer to a string using a lookup table (list) in my code.
Both the integer and the list is passed on from a COM and bound to observables in my code.
<ListView Name="IdList" MaxWidth="310" Height="190" Margin="5" SelectionMode="Single"
ItemsSource="{Binding Path=TypeItem.Ids}">'
<GridViewColumn Width="Auto"
DisplayMemberBinding="{Binding Path=ShipType,
Converter={StaticResource ShipTypeConverter},
ConverterParameter={x:Static vm:ConfigStaticItem.alternatives_shiptype}}"/>`
I have tried using multibinding, but only got DependencyProperty.UnsetValue for the list value
<GridViewColumn Width="Auto">
<GridViewColumnHeader Content="ShipType"/>
<GridViewColumn.DisplayMemberBinding >
<MultiBinding Converter="{StaticResource ShipTypeMultiConverter}">
<Binding Path="ShipType"/>
<Binding Path="ConfigStaticItem.alternatives_shiptype"/>
</MultiBinding>
</GridViewColumn.DisplayMemberBinding>
</GridViewColumn>
[ValueConversion(typeof(byte), typeof(string))]
public class ShipTypeMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
byte state = (byte)values[0];
List<StaticId> list = (List<StaticId>)values[1];
return state.ToString();
}
catch
{
return "";
}
}
Also tried using templates, but I think I got lost in the XAML :).
vm is a reference to my ViewModel
the TypeItem.Ids is defined as List, where Static is an observable class containing amongst other stuff the ShipType value
Does anybody have any suggestions how to solve this?enter code here
And the solution was:
Adding a source to my resource dictionary
<CollectionViewSource
x:Key="ShipTypeSource"
Source="{Binding ConfigStaticItem.alternatives_shiptype}"
/>
Using a standard binding with the static resource as converter parameter
<GridViewColumn
Width="Auto"
Header="Ship Type"
DisplayMemberBinding="{Binding Path=ShipType, Converter={StaticResource ShipTypeConverter},
ConverterParameter={StaticResource ShipTypeSource}}"
/>
And using a singlevalue converter
[ValueConversion(typeof(byte), typeof(string))]
public class ShipTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
Alternatives ShipTypes = (Alternatives)((parameter as CollectionViewSource).Source);
byte Type = (byte)value;
foreach (var key in ShipTypes)
{
if ((uint)Type == key.Key)
{
return key.Value;
}
}
return ""; // Ship type is undefined
}
catch
{
return "";
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
Related
Can I pass an index number into this list (SomeList)?
FontSize="{Binding FontSize, Source={x:Static ut:ViewSetupData.SomeList}, FallbackValue=12}"
You can put a constant indexer in the Path:
{Binding Path=[(sys:Int32)0], Source={x:Static ut:ViewSetupData.SomeList}}
But you can't bind a property of a Binding, so there's no way to stuff a parameter in there. However, you can combine multiple bindings in a MultiBinding, so you could use one of those with a multi-value converter:
C#:
public class IListIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// You might want a little more error-checking than this...
return ((IList)values[0])[(int)values[1]];
}
public virtual object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return null;
}
}
XAML:
<TextBlock>
<TextBlock.Resources>
<local:IListIndexerConverter x:Key="ListIndexer" />
</TextBlock.Resources>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ListIndexer}">
<Binding Source="{x:Static ut:ViewSetupData.SomeList}" />
<Binding
ElementName="MyComboBox"
Path="SelectedIndex"
/>
</MultiBinding>
</TextBlock.Test>
</TextBlock>
Update
While you were marking this as the solution, I was writing a more complete solution that addressed your need to grab a property from the list item:
C#:
public class ListItemPropertyGetter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var list = values[0] as IList;
var index = (int)(values[1] ?? 0);
var propname = values[2] as String;
object item = list[index];
var prop = item.GetType().GetProperty(propname);
var propvalue = prop.GetValue(item);
return propvalue;
}
catch
{
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
// Gotta put these somewhere
public static List<FontSizeThing> FontSizeThings { get; } =
new List<FontSizeThing>
{
new FontSizeThing(10),
new FontSizeThing(10.5),
new FontSizeThing(11),
new FontSizeThing(12),
new FontSizeThing(14),
new FontSizeThing(15),
};
}
public class FontSizeThing {
public FontSizeThing(double n) { FontSize = n; }
public double FontSize { get; set; }
}
XAML:
<ComboBox x:Name="FontSizeOptionCombo">
<sys:Int32>0</sys:Int32>
<sys:Int32>1</sys:Int32>
<sys:Int32>2</sys:Int32>
<sys:Int32>3</sys:Int32>
<sys:Int32>4</sys:Int32>
</ComboBox>
<TextBlock Text="Testing">
<TextBlock.Resources>
<hconv:ListItemPropertyGetter x:Key="ListItemPropertyGetter" />
</TextBlock.Resources>
<TextBlock.FontSize>
<MultiBinding Converter="{StaticResource ListItemPropertyGetter}" StringFormat="{}{0}">
<Binding Source="{x:Static hconv:ListItemPropertyGetter.FontSizeThings}" />
<Binding ElementName="FontSizeOptionCombo" Path="SelectedItem" />
<Binding Source="FontSize" />
</MultiBinding>
</TextBlock.FontSize>
</TextBlock>
FINAL UPDATE
Note that if I had merely populated FontSizeOptionCombo with the FontThings themselves, I could very simply have bound like this:
<ComboBox
x:Name="OtherCombo"
ItemsSource="{x:Static hconv:ListItemPropertyGetter.FontSizeThings}"
DisplayMemberPath="FontSize"
FontSize="{Binding SelectedItem.FontSize, ElementName=OtherCombo, FallbackValue=20}"
/>
If that fits in with what you're doing, it's by far the nicest way.
I have the following scenario
1- A combobox in xaml
<ComboBox
x:Name="PublishableCbo" Width="150" IsEnabled="True" HorizontalAlignment="Left" Height="20"
SelectedValue="{Binding Path=Published, Mode=TwoWay}"
Grid.Column="6" Grid.Row="0">
<ComboBox.Items>
<ComboBoxItem Content="All" IsSelected="True" />
<ComboBoxItem Content="Yes" />
<ComboBoxItem Content="No" />
</ComboBox.Items>
2- In a model class, I defined a property and bind to the selectedvalue in combobox
public bool Published
{
get
{
return _published;
}
set
{
_published = value;
OnPropertyChanged("Published");
}
}
I know I have to implement a converter, but don't know exactly how. What I want is when a select Yes/No, in the model get a True/false value, when "all" is selected, to get null value.
In order to be able to assign null to the Published property, you would have to change its type to Nullable< bool > (you can write bool? in C#).
public bool? Published
{
...
}
The Converter could be implemented so that it converts from string to bool and vice versa, perhaps like shown below. Note that the converter uses bool, not bool? since the value is passed to and from the converter as object, and hence boxed anyway.
public class YesNoAllConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
object result = "All";
if (value is bool)
{
result = (bool)value ? "Yes" : "No";
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
object result = null;
switch ((string)value)
{
case "Yes":
result = true;
break;
case "No":
result = false;
break;
}
return result;
}
}
To enable the use of this converter, you have to change your ComboBox item type to string, and bind to the SelectedItem property, not SelectedValue.
<ComboBox SelectedItem="{Binding Path=Published, Mode=TwoWay,
Converter={StaticResource YesNoAllConverter}}">
<sys:String>All</sys:String>
<sys:String>Yes</sys:String>
<sys:String>No</sys:String>
</ComboBox>
where sys is the following xml namespace declaration:
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Public Class View
Public Property Items As String() = {"One", "Two", "Three"}
Public Property Index As Integer = 0
End Class
It's instance is set as DataContext of this XAML:
<Window>
<StackPanel>
<ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding Index}"/>
<Label Content="{Binding Items[Index]}"/>
</StackPanel>
</Window>
But this doesn't work.
<Label Content="{Binding Items[{Binding Index}]}"/>
This neither.
<Label Content="{Binding Items[0]}"/>
This works.
Is there any solution except making extra property in view? Something directly in XAML?
I'm afraid it's not possible without some code-behind, but using reflection and dynamic, you can create a converter that can do this (it would be possible without dynamic, but more complex):
public class IndexerConverter : IValueConverter
{
public string CollectionName { get; set; }
public string IndexName { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Type type = value.GetType();
dynamic collection = type.GetProperty(CollectionName).GetValue(value, null);
dynamic index = type.GetProperty(IndexName).GetValue(value, null);
return collection[index];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Put following into resources:
<local:IndexerConverter x:Key="indexerConverter" CollectionName="Items" IndexName="Index" />
and use it like this:
<Label Content="{Binding Converter={StaticResource indexerConverter}}"/>
EDIT: The previous solution doesn't update properly when the values change, this one does:
public class IndexerConverter : IMultiValueConverter
{
public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
{
return ((dynamic)value[0])[(dynamic)value[1]];
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
In resources:
<local:IndexerConverter x:Key="indexerConverter"/>
Usage:
<Label>
<MultiBinding Converter="{StaticResource indexerConverter}">
<Binding Path="Items"/>
<Binding Path="Index"/>
</MultiBinding>
</Label>
What you write in the binding markup extension is assigned to the Path property by default, this property is a string so any dynamic content you refer to inside it will not be evaluated. There is no simple XAML-only method to do what you try to do.
Why don't use this:
<StackPanel>
<ListBox Name="lsbItems" ItemsSource="{Binding Items}" SelectedIndex="{Binding Index}"/>
<Label Content="{Binding ElementName=lsbItems, Path=SelectedItem}"/>
</StackPanel>
Say I've got some TextBlocks on my UI, something like so:
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding DessertIndex}" />
<TextBlock Text="{Binding Food[2]}" />
<TextBlock Text="{Binding Food[{Binding DessertIndex}]}" />
</StackPanel>
and in my code behind I've got something like this:
public partial class MainWindow : Window
{
public int DessertIndex
{
get { return 2; }
}
public object[] Food
{
get
{
return new object[]{"liver", "spam", "cake", "garlic" };
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
The first two TextBlocks display fine for me, displaying 2 and 'cake' respectively. The third one doesn't accomplish what I'd like, namely use the DessertIndex property to index into that array and also display 'cake'. I did a little searching here on SO for a similar question but didn't find one. Ultimately, I don't want to specify values like 2 in my .xaml file and would like to rely upon a property instead for indexing into that array. Is this possible? If so, what am I doing wrong here?
EDIT:
So what I more closely have is a situation where the data is a List of these object[] and I'm using the above StackPanel as part of a DataTemplate for a ListBox. So the idea, as Mark Heath suggests below, of using a property that dereferences the array doesn't seem to work as I'd want. Ideas?
Another alternative is to use MultiBinding with a converter:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<local:FoodIndexConverter x:Key="foodIndexConverter" />
</StackPanel.Resources>
<TextBlock Text="{Binding DessertIndex}" />
<TextBlock Text="{Binding Food[2]}" />
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource foodIndexConverter}">
<Binding Path="DessertIndex" />
<Binding Path="Food"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Window>
Then in the code-behind, the converter is defined something like this:
namespace WpfApplication1
{
public class FoodIndexConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null || values.Length != 2)
return null;
int? idx = values[0] as int?;
object[] food = values[1] as object[];
if (!idx.HasValue || food == null)
return null;
return food[idx.Value];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
if you are going to the trouble of having a DesertIndex property on your DataContext, why not a property that dereferences the Food array with DesertIndex:
public object SelectedFood
{
get { return Food[DessertIndex]; }
}
public int DessertIndex
{
get { return 2; }
}
public object[] Food
{
get
{
return new object[]{"liver", "spam", "cake", "garlic" };
}
}
then you can bind directly to that:
<TextBlock Text="{Binding SelectedFood}" />
This is essentially the "MVVM" approach: make the datacontext object have properties that are just right for binding to.
Just To add on the great answer by Colin Thomsen.
You could also use C# dynamic keyword to make this solution work with pretty much every container type. Or even bind to multidimensional containers "{Binding Food[{Binding DessertIndex1}][{Binding DessertIndex2}]}"
public class ContainerDoubleAccessConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
try
{
dynamic idx1 = values[0];
dynamic idx2 = values[1];
dynamic container = values[2];
return container[idx1][idx2];
}
catch (System.Exception err)
{
DebugTrace.Trace("bad conversion " + err.Message);
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
The obvious solution would be to have a row number property on a ModelView element, but the drawback is that you have to re-generate those when you add records or change sort order.
Is there an elegant solution?
I think you have the elegant solution, but this works.
XAML:
<ListView Name="listviewNames">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn
Header="Number"
DisplayMemberBinding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource IndexConverter}}" />
<GridViewColumn
Header="Name"
DisplayMemberBinding="{Binding Path=Name}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
ValueConverter:
public class IndexConverter : IValueConverter
{
public object Convert(object value, Type TargetType, object parameter, CultureInfo culture)
{
ListViewItem item = (ListViewItem) value;
ListView listView = ItemsControl.ItemsControlFromItemContainer(item) as ListView;
int index = listView.ItemContainerGenerator.IndexFromContainer(item);
return index.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
If you have a dynamic list where items are added, deleted or moved, you can still use this very nice solution and simply let the currentview of your listview refresh itself after the changements in your source list are done.
This code sample removes the current item directly in the data source list "mySourceList" (which is in my case an ObservableCollection) and finally updates the line numbers to correct values .
ICollectionView cv = CollectionViewSource.GetDefaultView(listviewNames.ItemsSource);
if (listviewNames.Items.CurrentItem != null)
{
mySourceList.RemoveAt(cv.CurrentPosition);
cv.Refresh();
}
First you need to set the AlternationCount to items count+1, for instance:
<ListView AlternationCount="1000" .... />
Then AlternationIndex will show the real index, even during the scrolling:
<GridViewColumn
Header="#" Width="30"
DisplayMemberBinding="{Binding (ItemsControl.AlternationIndex),
RelativeSource={RelativeSource AncestorType=ListViewItem}}" />
This will work like a charm,
I don't know about performance,
Still we can give it a try
Create a Multi Value Converter
public class NumberingConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values != null && values.Any() && values[0] != null && values[1] != null)
{
//return (char)(((List<object>)values[1]).IndexOf(values[0]) + 97);
return ((List<object>)values[1]).IndexOf(values[0]) + 1;
}
return "0";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}
and
your Xaml like this
<ItemsControl ItemsSource="{Binding ListObjType}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label>
<MultiBinding Converter="{StaticResource NumberingConvertor}">
<Binding Path="" />
<Binding Path="ItemsSource"
RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</Label>
<TextBlock Text="{Binding }" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Idea is to send Object and list both to the converter and let converter decide the number. You can modify converter to display ordered list.
Here is another way, including code comments that will help you understand how it works.
public class Person
{
private string name;
private int age;
//Public Properties ....
}
public partial class MainWindow : Window
{
List<Person> personList;
public MainWindow()
{
InitializeComponent();
personList= new List<Person>();
personList.Add(new Person() { Name= "Adam", Agen= 25});
personList.Add(new Person() { Name= "Peter", Agen= 20});
lstvwPerson.ItemsSource = personList;
//After updates to the list use lstvwPerson.Items.Refresh();
}
}
The XML
<GridViewColumn Header="Number" Width="50"
DisplayMemberBinding="{
Binding RelativeSource= {RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}},
DELETE Path=Content, DELETE
Converter={StaticResource IndexConverter},
ConverterParameter=1
}"/>
RelativeSource is used in particular binding cases when we try to bind a property of a given object to another property of the object itself [1].
Using Mode=FindAncestor we can traverse the hierarchy layers and get a specified element, for example the ListViewItem (we could even grab the GridViewColumn). If you have two ListViewItem elements you can specify which you want with "AncestorLevel = x".
Path: Here I simply take the content of the ListViewItem (which is my object "Person").
Converter Since I want to display row numbers in my Number column and not the object Person I need to create a Converter class which can somehow transform my Person object to a corresponding number row. But its not possible, I just wanted to show that the Path goes to the converter. Deleting the Path will send the ListViewItem to the Converter.
ConverterParameter Specify a parameter you want to pass to the IValueConverter class. Here you can send the state if you want the row number to start at 0,1,100 or whatever.
public class IndexConverter : IValueConverter
{
public object Convert(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
{
//Get the ListViewItem from Value remember we deleted Path, so the value is an object of ListViewItem and not Person
ListViewItem lvi = (ListViewItem)value;
//Get lvi's container (listview)
var listView = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;
//Find out the position for the Person obj in the ListView
//we can get the Person object from lvi.Content
// Of course you can do as in the accepted answer instead!
// I just think this is easier to understand for a beginner.
int index = listView.Items.IndexOf(lvi.Content);
//Convert your XML parameter value of 1 to an int.
int startingIndex = System.Convert.ToInt32(parameter);
return index + startingIndex;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I found solution that will work even in case when you need to move your elements inside the collection. So actually what we need to do for it is notify dummy property "ListNumbersNotify" every time our collection is changed and bind everything with that tricky MultiBinding converter.
XAML:
<Window ...
x:Name="This">
...
<ListView Name="ListViewCurrentModules">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label>
<MultiBinding Converter="{helpers:NumberingConvertor}">
<Binding Path="" />
<Binding ElementName="ListViewCurrentModules" />
<Binding Path="ListNumbersNotify" ElementName="This" />
</MultiBinding>
</Label>
<Border>
...
</Border>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Converter:
public abstract class MultiConvertorBase<T> : MarkupExtension, IMultiValueConverter
where T : class, new()
{
public abstract object Convert(object[] values, Type targetType, object parameter, CultureInfo culture);
public virtual object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (_converter == null)
_converter = new T();
return _converter;
}
private static T _converter = null;
}
public class NumberingConvertor : MultiConvertorBase<NumberingConvertor>
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return ((ListView)values[1]).Items.IndexOf(values[0]) + 1;
}
}
Code behind:
public partial class AddModulesWindow: Window, INotifyPropertyChanged
{
...
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string prop)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
public object ListNumbersNotify { get; }
public AddModulesWindow(ICore core)
{
InitializeComponent();
this.core = core;
CurrentModuleInfos = new ObservableCollection<ModuleInfo>(core.Modules.Select(m => m?.ModuleInfo));
CurrentModuleInfos.CollectionChanged += CurrentModuleTypes_CollectionChanged;
ListViewCurrentModules.ItemsSource = CurrentModuleInfos;
}
private void CurrentModuleTypes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged("ListNumbersNotify");
}
It's the addition to answer of amaca for problems found by Allon Guralnek and VahidN. Scrolling problem is solved with setting ListView.ItemsPanel to StackPanel in XAML:
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
This replacement of default VirtualizingStackPanel with simple StackPanel disables automatic regeneration of internal collection of ListViewItem. So indices would not chaotically change when scrolling. But this replacement can decrease perfomance on large collections. Also, dynamic numeration changes can be achieved with call CollectionViewSource.GetDefaultView(ListView.ItemsSource).Refresh() when ItemsSource collection changed. Just like with ListView filtering. When I tried to add handler with this call on event INotifyCollectionChanged.CollectionChanged my ListView output was duplicating last added row (but with correct numeration). Fixed this by placing refresh call after every collection change in code. Bad solution, but it works perfect for me.
amaca answer is great for static lists. For dynamic:
We should use MultiBinding, second binding is for changing collection;
After deleting ItemsControl not contains deleted object, but ItemContainerGenerator contains.
Converter for dynamic lists (I use it for TabControl TabItem's):
public class TabIndexMultiConverter : MultiConverterBase
{
public override object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
{
TabItem tabItem = value.First() as TabItem;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(tabItem);
object context = tabItem?.DataContext;
int idx = ic == null || context == null // if all objects deleted
? -1
: ic.Items.IndexOf(context) + 1;
return idx.ToString(); // ToString necessary
}
}
By following best answer solution I found an issue when indexes still not updated after removing/replacing items inside list view. To solve that there is one not so clear hint (I propose to use it in small collections): after executing item removing/replacing you should invoke ObservableCollection(INotifyCollectionChanged).CollectionChanged event with Reset action. This is possible to make with extending existing ObservableCollection, which is ItemsSource or use reflection when this is not possible.
Ex.
public class ResetableObservableCollection<T> : ObservableCollection<T>
{
public void NotifyReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
private void ItemsRearranged()
{
Items.NotifyReset();
}
Here's my little converter which works great as of WPF in 2017 with .NET 4.7.2, including with the VirtualizingStackPanel fully enabled:
[ValueConversion(typeof(IList), typeof(int))]
public sealed class ItemIndexConverter : FrameworkContentElement, IValueConverter
{
public Object Convert(Object data_item, Type t, Object p, CultureInfo _) =>
((IList)DataContext).IndexOf(data_item);
public Object ConvertBack(Object o, Type t, Object p, CultureInfo _) =>
throw new NotImplementedException();
};
Add an instance of this IValueConverter to the Resources of the GridViewColumn.CellTemplate, or elsewhere. Or, instantiate it in-situ on the Binding of the bound element, like I show here. In any case, you need to create an instance of the ItemIndexConverter and don't forget to bind the whole source collection to it. Here I'm pulling a reference to the source collection out of the ItemsSource property of the ListView--but this entails some unrelated hassles over accessing the XAML root, so if you have a better and easier way to refer to the source collection, you should do so.
As for accessing a property on the XAML root, the ListView root in XAML is given the name w_root, and the XAML markup extension {x:Reference ...} is used to access the XAML root element. I don't think "ElementName" binding will work here since the reference occurs in a template context.
<ListView x:Class="myApp.myListView"
x:Name="w_root"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:myApp"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<ListView.View>
<GridView>
<GridViewColumn Width="50">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<Binding>
<Binding.Converter>
<local:ItemIndexConverter DataContext="{Binding
Source={x:Reference w_root},
Path=(ItemsControl.ItemsSource)}" />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
That's it! It seems to work pretty quickly with a large number of rows, and again, you can see that the reported indices are correct when arbitrarily scrolling around, and that VirtualizingStackPanel.IsVirtualizing is indeed set to True.