How Do You Bind To An Item In A Dictionary? - wpf

I'm using the RadCarousel control from Telerik in a WPF application with C#. RadCarousel is similar to a GridView in that it binds to a collection and show's each item in the collection (so my question isn't specific to Telerik or RadCarousel).
I'm using a DataTemplate to specify how each record should get displayed.
If I do this it work's fine:
<DataTemplate>
<TextBlock Text="{Binding Path=oMySubObject.TheAmount}" />
</DataTemplate>
But what if I need to point to an item in a dictionary?
<DataTemplate>
<TextBlock Text="{Binding Path=myDictionaryOfSubObjects[TheCurrentIndex].TheAmount}" />
</DataTemplate>
This I can't get working and don't know how. Basically...I need the index to be specified at runtime and when it gets updated, the binding updates.
Any advice?

<Window.Resources>
<NAMESPACEWHERECONVERTERRESIDES:DictionaryConverter x:Key="cDictionaryConverter"/>
</WindowResources>
<TextBlock Text="{Binding Path=myDictionaryOfSubObjects, Converter={StaticResource cDictionaryConverter}}"/>
// Something like this:
[ValueConversion(typeof(Dictionary), typeof(string))]
public class DictionaryConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Dictionary<type, type> dict = value as Dictionary<type, type>;
return dict[CurrentIndex].TheAmount;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return 0;
}
}

You can only use constant values in the indexer, TheCurrentIndex will not be resolved. There are a few workarounds, like passing the dictionary and the index to a multi value converter to resolve the item there.

With anything complicated, you should probably make a getter that does the job, and bind to that.
What is "TheCurrentIndex"?

Window1.xaml
<Window x:Class="QuizBee.Host.Window1"
x:Name="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ListView ItemsSource="{Binding ElementName=Window1, Path=myDictionary}" />
</Window>
Window1.xaml.cs
public partial class Window1:Window
{
// the property must be public, and it must have a getter & setter
public Dictionary<string, myClass> myDictionary { get; set; }
public Window1()
{
// define the dictionary items in the constructor
// do the defining BEFORE the InitializeComponent();
myDictionary = new Dictionary<string, myClass>()
{
{"item 1", new myClass(1)},
{"item 2", new myClass(2)},
{"item 3", new myClass(3)},
{"item 4", new myClass(4)},
{"item 5", new myClass(5)},
};
InitializeComponent();
}
}

Related

Binding WPF ComboBox to Dictionary<String,ObservableCollection<String>>

In my ViewModel, I have a Dictionary of observable string collections, declared as below.
public Dictionary<string, ObservableCollection<string>> NamePartsDict { get; set; }
I would like to bind the Dictionary to ComboBoxes in my user interface in such a way that each ComboBox can 'select' which collection to bind to.
So in my XAML, I would like to use:
<ComboBox x:Name="comboBox" IsEditable="True"
ItemsSource="{Binding CurrentLibrary.NamePartsDict[Year]}" Margin="80,0,0.871,0"></ComboBox>
which I would expect to bind to the Collection that is indexed by the Key "Year", and populate the ComboBox with the strings stored in the collection.
However, this XAML results in an empty ComboBox.
I have verified that the Dictionary itself can be bound. The below XAML populates the ComboBox with the string representations of each Key, Value pair.
<ComboBox x:Name="comboBox" IsEditable="True"
ItemsSource="{Binding CurrentLibrary.NamePartsDict}" Margin="80,0,0.871,0"></ComboBox>
Is something in my binding path wrong when obtaining the value from the dictionary? Or am I trying to do something that isn't possible (in which case I'll have to find another way!)?
Any help much appreciated!
Tim
You should use a Datatemplate here
like in here:
http://www.codeproject.com/Articles/47923/Using-a-different-DataTemple-when-a-WPF-ComboBox-i
I solved this problem by using a converter:
/// <summary>
/// Returns a
/// </summary>
public class DomainValueConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ObservableCollection<DomainValue> retVal = null;
DomainDefinitionCollection dds = value as DomainDefinitionCollection;
if (dds != null)
{
retVal = dds[parameter.ToString()];
}
return retVal;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
<controls:ComboBox VerticalAlignment="Center"
DisplayMemberPath="Name"
SelectedValuePath="Value"
SelectedValue="{Binding ChildClientEntity.EditableEntity.EditableAttributes.TeamPosition, Mode=TwoWay}"
ItemsSource="{Binding ChildClientEntity.Domains, Converter={StaticResource DomainValueConverter}, ConverterParameter=SiteVisitTeamPosition}" />
where the ConverterParameter is the key into the dictionary, Domains.
public Dictionary<string, ObservableCollection<DomainValue>> Domains { get; private set; }

wpf IValueConverter not updating the view

I am trying to use IValueConverter to convert the collection into proxy object for data binding.
The converter seems to work fine, but the problem is when a new object is added or removed from the collection. The same is not refreshed in the view..
Model Object:
public class A {
public ObservableCollection<string> Members { get; }
}
Converter
public class MemberConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var collection = new CompositeCollection();
var a = value as A;
a.Members.ToList().ForEach(member => {
collection.Add(new ProxyClass{ A= a, Member= member });
});
return collection;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new System.NotImplementedException();
}
}
Proxy Class
public class ProxyClass {
public A A { get; set; }
public string Member{ get; set; }
}
XAML:
<DataTemplate DataType="{x:Type my:ProxyClass}">
<TextBlock Text="{Binding Path=Member}"/>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type A}" ItemsSource="{Binding Converter={StaticResource MemberConverter}}">
<TextBlock Text ="{Binding}"/>
</HierarchicalDataTemplate>
A Binding will only be re-evaluated if a property change notification for the property to which it is bound has been changed. In this case the ItemsSource is bound to the DataContext - the A instance itself - so unless it is given a new A instance it will not be re-evaluated. There is nothing listening to the change notifications that the collection raises since the value given to the ItemsSource is actually a different collection instance that you are creating within your converter.
One option would be to have the converter create a helper class that hooks the CollectionChanged event of the source collection (i.e. the value passed into the converter) and that object would be responsible for keeping the source collection and the one it creates in sync. Another option is to try to force the binding to get re-evaluated - e.g. use a Path of "Members" for the ItemsSource binding and when you change the contents of the collection raise a property change notification for "Members" on A.
Its not updating because your A Property doesn't implement INotifyPropertyChanged or is a DependencyProperty
If need be you can add the following after making it implement one of the previous.
ItemsSource="{Binding Converter={StaticResource MemberConverter}, UpdateSourceTrigger=PropertyChanged}">

WPF binding ComboBox to enum (with a twist)

Well the problem is that I have this enum, BUT I don't want the combobox to show the values of the enum. This is the enum:
public enum Mode
{
[Description("Display active only")]
Active,
[Description("Display selected only")]
Selected,
[Description("Display active and selected")]
ActiveAndSelected
}
So in the ComboBox instead of displaying Active, Selected or ActiveAndSelected, I want to display the DescriptionProperty for each value of the enum. I do have an extension method called GetDescription() for the enum:
public static string GetDescription(this Enum enumObj)
{
FieldInfo fieldInfo =
enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
{
return enumObj.ToString();
}
else
{
DescriptionAttribute attrib =
attribArray[0] as DescriptionAttribute;
return attrib.Description;
}
}
So is there a way I can bind the enum to the ComboBox AND show it's content with the GetDescription extension method?
Thanks!
I would suggest a DataTemplate and a ValueConverter. That will let you customize the way it's displayed, but you would still be able to read the combobox's SelectedItem property and get the actual enum value.
ValueConverters require a lot of boilerplate code, but there's nothing too complicated here. First you create the ValueConverter class:
public class ModeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
return ((Mode) value).GetDescription();
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
Since you're only converting enum values to strings (for display), you don't need ConvertBack -- that's just for two-way binding scenarios.
Then you put an instance of the ValueConverter into your resources, with something like this:
<Window ... xmlns:WpfApplication1="clr-namespace:WpfApplication1">
<Window.Resources>
<WpfApplication1:ModeConverter x:Key="modeConverter"/>
</Window.Resources>
....
</Window>
Then you're ready to give the ComboBox a DisplayTemplate that formats its items using the ModeConverter:
<ComboBox Name="comboBox" ...>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource modeConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
To test this, I threw in a Label too, that would show me the actual SelectedItem value, and it did indeed show that SelectedItem is the enum instead of the display text, which is what I would want:
<Label Content="{Binding ElementName=comboBox, Path=SelectedItem}"/>
I like the way you think. But GetCustomAttributes uses reflection. What is that going to do to your performance?
Check out this post:
WPF - Displaying enums in ComboBox control
http://www.infosysblogs.com/microsoft/2008/09/wpf_displaying_enums_in_combob.html
This is how I am doing it with MVVM. On my model I would have defined my enum:
public enum VelocityUnitOfMeasure
{
[Description("Miles per Hour")]
MilesPerHour,
[Description("Kilometers per Hour")]
KilometersPerHour
}
On my ViewModel I expose a property that provides possible selections as string as well as a property to get/set the model's value. This is useful if we don't want to use every enum value in the type:
//UI Helper
public IEnumerable<string> VelocityUnitOfMeasureSelections
{
get
{
var units = new []
{
VelocityUnitOfMeasure.MilesPerHour.Description(),
VelocityUnitOfMeasure.KilometersPerHour.Description()
};
return units;
}
}
//VM property
public VelocityUnitOfMeasure UnitOfMeasure
{
get { return model.UnitOfMeasure; }
set { model.UnitOfMeasure = value; }
}
Furthermore, I use a generic EnumDescriptionCoverter:
public class EnumDescriptionConverter : IValueConverter
{
//From Binding Source
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is Enum)) throw new ArgumentException("Value is not an Enum");
return (value as Enum).Description();
}
//From Binding Target
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is string)) throw new ArgumentException("Value is not a string");
foreach(var item in Enum.GetValues(targetType))
{
var asString = (item as Enum).Description();
if (asString == (string) value)
{
return item;
}
}
throw new ArgumentException("Unable to match string to Enum description");
}
}
And finally, with the view I can do the following:
<Window.Resources>
<ValueConverters:EnumDescriptionConverter x:Key="enumDescriptionConverter" />
</Window.Resources>
...
<ComboBox SelectedItem="{Binding UnitOfMeasure, Converter={StaticResource enumDescriptionConverter}}"
ItemsSource="{Binding VelocityUnitOfMeasureSelections, Mode=OneWay}" />
I suggest you use a markup extension I had already posted here, with just a little modification :
[MarkupExtensionReturnType(typeof(IEnumerable))]
public class EnumValuesExtension : MarkupExtension
{
public EnumValuesExtension()
{
}
public EnumValuesExtension(Type enumType)
{
this.EnumType = enumType;
}
[ConstructorArgument("enumType")]
public Type EnumType { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (this.EnumType == null)
throw new ArgumentException("The enum type is not set");
return Enum.GetValues(this.EnumType).Select(o => GetDescription(o));
}
}
You can then use it like that :
<ComboBox ItemsSource="{local:EnumValues local:Mode}"/>
EDIT: the method I suggested will bind to a list of string, which is not desirable since we want the SelectedItem to be of type Mode. It would be better to remove the .Select(...) part, and use a binding with a custom converter in the ItemTemplate.
Questions of using reflection and attributes aside, there are a few ways you could do this, but I think the best way is to just create a little view model class that wraps the enumeration value:
public class ModeViewModel : ViewModel
{
private readonly Mode _mode;
public ModeViewModel(Mode mode)
{
...
}
public Mode Mode
{
get { ... }
}
public string Description
{
get { return _mode.GetDescription(); }
}
}
Alternatively, you could look into using ObjectDataProvider.
I've done it like this :
<ComboBox x:Name="CurrencyCodeComboBox" Grid.Column="4" DisplayMemberPath="." HorizontalAlignment="Left" Height="22" Margin="11,6.2,0,10.2" VerticalAlignment="Center" Width="81" Grid.Row="1" SelectedValue="{Binding currencyCode}" >
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
in code I set itemSource :
CurrencyCodeComboBox.ItemsSource = [Enum].GetValues(GetType(currencyCode))

How to display row numbers in a ListView?

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.

Silverlight Databinding - Binding a ValueConverter to a property on a view-model

Let's pretend I have the following xaml...
<UserControl.Resources>
<local:ViewModel x:Name="viewModel" />
<local:LoadChildrenValueConverter x:Name="valueConverter" />
</UserControl.Resources>
<UserControl.DataContext>
<Binding Source="{StaticResource viewModel}" />
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<control:TreeView ItemsSource="{Binding Root}">
<control:TreeView.ItemTemplate>
<control:HierarchicalDataTemplate ItemsSource="{Binding Converter={StaticResource valueConverter}}">
<TextBlock Text="{Binding}" />
</control:HierarchicalDataTemplate>
</control:TreeView.ItemTemplate>
</control:TreeView>
</Grid>
...and the following code to go with it...
using System;
using System.Collections.ObjectModel;
using System.Windows.Data;
namespace SilverlightViewModelSpike
{
public class ViewModel
{
public ViewModel()
{
Root = new ObservableCollection() { "Item 1", "Item 2", "Item 3", };
}
public ObservableCollection Root { get; private set; }
}
public class LoadChildrenValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new ObservableCollection() { "Item 1", "Item 2", "Item 3", };
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
This works as expected, but it feels wrong that I have two separate classes that are required in order to grab the needed data for my view (imagine that ViewModel and LoadChildrenValueConverter pulled data from a web service instead of returning hard coded data). Is there a better solution here? I was thinking maybe something like this...
using System;
using System.Collections.ObjectModel;
using System.Windows.Data;
namespace SilverlightViewModelSpike
{
public class ViewModel
{
public ViewModel()
{
Root = new ObservableCollection() { "Item 1", "Item 2", "Item 3", };
ValueConverter = new LoadChildrenValueConverter();
}
public ObservableCollection Root { get; private set; }
public LoadChildrenValueConverter ValueConverter { get; private set; }
}
public class LoadChildrenValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new ObservableCollection() { "Item 1", "Item 2", "Item 3", };
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
... but then i can't get this line to work...
<control:HierarchicalDataTemplate ItemsSource="{???}">
...and even that doesn't seem like a great solution. Does anyone have a nice clean solution for this?
Since you're using a ViewModel to sit between your actual model and your view, I wonder if it's easier just to implement the IValueConverter logic directly in there. Sort of like:
public class ViewModel
{
public ObservableCollection Root { get; set: }
public ObservableCollection Children
{
get { /* return children items */ }
}
}
Then you can simply bind directly to your second property:
<control:HierarchicalDataTemplate ItemsSource="{Binding Children}">
I think the main purpose of a ViewModel object is to limit the number of "tricks" (such as IValueConverters) you need to pull to get the data you need from the original model. Since you have one, you might as well make use of it.
Edit 1
... and of course now that I re-read your post I see that there's more to it. You're getting the children for each item in your "Root" collection.
How about implementing the IValueConverter as a static instance in your ViewModel itself?
public class ViewModel : IValueConverter
{
public static readonly IValueConverter ChildrenConverter
= new LoadChildrenValueConverter();
}
Now you should be able to say:
<control:HierarchicalDataTemplate
ItemsSource="{Binding Converter={x:Static local:ViewModel.ChildrenConverter}}">
Edit 2
Ok, you're using Silverlight so {x:Static} isn't available to you.
The only other option I can think of that will let you reuse the one static resource instead of having to declare two is to implement the IValueConverter directly in your ViewModel. This is no good if you will need to do more than one type of conversion, but if your ViewModel is very narrow-focused then it could do the job. So:
public class ViewModel : IValueConverter
{
// move your Convert and ConvertBack methods into here
}
Now you can do this:
<control:HierarchicalDataTemplate
ItemsSource="{Binding Converter={StaticResource ViewModel}}">
Sorry guys I am a little confused about what you are trying to do here... Anyway, from the title it sounds as if you want to a property in your value converter to a property in your value converter. Firstly have a look at an article I have written explaining exactly how you can do that:
http://nick-howard.blogspot.com/2011/06/silverlight-4-value-converter.html
So what you would do is create an ObvervableCollection dependency property in your LoadChildrenValueConverter, for arguments sake let’s call it Children.
Then in your xaml you can change the LoadChildrenValueConverter to something like this:
That way you are only calling the web service once from your view model, and you are then sharing that ObvervableCollection in your view model with your value converter.
Hope that helps.

Resources