How do I obtain the Index of a ListViewItem? - wpf

I have a list view that uses a collectionsource and wish to set the background colour of the ListViewItems depending upon whether their index in the list matches a value held in the view model. To do this I am binding the background of the listVeiwItem to the value in the ViewModel, and using a converter to determine the background colour. To make this determination the converter needs to be passed the ListViewItem's index. How do I obtain the index using XAML?
Here is the XAML for the data template used by the ListView:
<DataTemplate x:Key="ILMemberTemplate">
<StackPanel Orientation="Horizontal" Background="{Binding Path=ListIndex, Mode=OneWay, Converter={StaticResource ParticipantBackground}, ConverterParameter={???}}">
<TextBlock
Width="200"
TextAlignment="Left"
Foreground="{Binding Path=IsPC, Mode=OneWay, Converter={StaticResource ParticipantColour}}"
Text="{Binding Path=Name, Mode=OneWay}"/>
<TextBlock
Width="40"
TextAlignment="Center"
Foreground="{Binding Path=IsPC, Mode=OneWay, Converter={StaticResource ParticipantColour}}"
Text="{Binding Path=Initiative, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>

You can't obtain the index in XAML and pass it as a ConverterParameter. This is not possible. But you can use a multi converter and bind to both the ListIndex and the parent ListViewItem container, e.g.:
class MultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var listIndex = values[0];
//...
ListViewItem item = value as ListViewItem;
ListView lv = FindParent<ListView>(item);
ICollectionView view = lv.ItemsSource as ICollectionView;
IEnumerator e = view.GetEnumerator();
int index = 0;
while (e.MoveNext())
{
if (Equals(item.DataContext, e.Current))
return index;
else
index++;
}
//return some brush based on the indexes..
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null) return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
}
XAML:
<DataTemplate x:Key="ILMemberTemplate">
<StackPanel Orientation="Horizontal">
<StackPanel.Background>
<MultiBinding Converter="{StaticResource multiConverter}">
<Binding Path="ListIndex" />
<Binding Path="." RelativeSource="{RelativeSource AncestorType=ListViewItem}" />
</MultiBinding>
</StackPanel.Background>
<!-- ... -->
</StackPanel>
</DataTemplate>

Related

WPFLocalizationExtension with ItemTemplate

I'm using WPFLocalizationExtension for my WPF app.
I have one ComboBox for languages selection. Item source is an ObservableCollection<KeyValuePair<string, string>> as below:
TITLE_LANGUAGE_ENGLISH : en
TITLE_LANGUAGE_VIETNAMESE: vi-VN
This is my xaml code:
<TextBlock Text="{lex:Loc TITLE_LANGUAGE}"></TextBlock>
<ComboBox Grid.Column="1"
ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{lex:Loc Key={Binding Key}}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
When I run the application, it throws me an exeption as below:
A 'Binding' cannot be set on the 'Key' property of type 'LocExtension'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject
How can I translate the ItemTemplate ?
Thank you,
You could use an IMultiValueConverter together with a MultiBinding, so that you don't loose the ability to update the localization on-the-fly.
<ComboBox ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding>
<MultiBinding.Bindings>
<Binding Path="Key" Mode="OneTime"/>
<Binding Path="Culture" Source="{x:Static lex:LocalizeDictionary.Instance}"/>
</MultiBinding.Bindings>
<MultiBinding.Converter>
<l:TranslateMultiConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
And here is the converter:
class TranslateMultiConverter : DependencyObject, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2)
{
var key = values[0] as string;
if (key == null)
{
return DependencyProperty.UnsetValue;
}
var cultureInfo = (values[1] as CultureInfo) ?? culture;
return LocalizeDictionary.Instance.GetLocalizedObject(key, this, cultureInfo);
}
return values.FirstOrDefault();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The LocalizeDictionary will raise a PropertyChanged event when the app's language will change causing the MultiBinding to refresh the values.
Note that the converter is a DependencyObject too. This is to provide the context to the LocalizeDictionary when calling the GetLocalizedObject method.
you have to bind to the Path Key directly. The TextBlock at DataTemplate points directly to a single KeyValuePair object, that you can access the property Key directly.
<TextBlock Text="{lex:Loc TITLE_LANGUAGE}"></TextBlock>
<ComboBox Grid.Column="1"
ItemsSource="{Binding AvailableLanguages}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Update:
Maybe you have to add a Converter. Try WPFLocalizeExtension.TypeConverters.DefaultConverter or implement a class deriving from IValueConverter by yourself.
<ComboBox.Resources>
<cv:DefaultConverter x:Key="DConv" />
</ComboBox.Resources>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Key, Converter={StaticResource DConv}}" />
</DataTemplate>
</ComboBox.ItemTemplate>

Unable to retrieve actual Width

I am having one list view
<ListView Grid.Row="4" Grid.ColumnSpan="2"
Margin="{Binding ElementName=BorderContainer, Path=Margin}"
FontSize="{DynamicResource PlaceHolderFontSize}"
ItemContainerStyle="{StaticResource ListItemContainerStyle}"
Foreground="Black"
>
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource BorderlessHeader}">
<GridViewColumn Header="Last Backup Date/Time" HeaderTemplate="{StaticResource BlueHeader}"
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListView}, Converter={StaticResource ColumnWidth},ConverterParameter=221}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Date}" ToolTip="{Binding Date}" FontSize="{DynamicResource ContentFontSize}" Style="{StaticResource GridColumText}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
As mentioned in the code am calculating the header width using converter. and inside my converter am using like below
public object Convert(object value, Type target Type, object parameter, Culture Info culture)
{
ListView listview = value as ListView;
double width = listview.ActualWidth;
but the listview.actualwidth is always giving "0" as result. Why this is happening? any ideas?
Why this is happening?
Probably because the ListView actually has an actual width of 0 by the time the Convert method gets invoked.
any ideas?
You could use a MultiBinding and an IMultiValueConverter that binds to both the ListView itself and its ActualWidth property:
<GridViewColumn Header="Last Backup Date/Time" HeaderTemplate="{StaticResource BlueHeader}">
<GridViewColumn.Width>
<MultiBinding Converter="{StaticResource ColumnWidth}" ConverterParameter="221">
<Binding Path="." RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" />
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" />
</MultiBinding>
</GridViewColumn.Width>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Date}" ToolTip="{Binding Date}" FontSize="{DynamicResource ContentFontSize}" Style="{StaticResource GridColumText}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
public class ColumnWidthConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ListView listview = values[0] as ListView;
double width = listview.ActualWidth;
if (width == 0)
return (double)parameter;
//...
return 100.0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then the converter should be invoked whenever the ActualWidth property is updated.
If you have for example 4 Columns, you can try the following:
XAML
<ListView x:Name="listView">
<ListView.View>
<GridView>
<GridViewColumn Width="{Binding ElementName=listView, Path=ActualWidth, Converter={StaticResource ColumnWidthConverter},ConverterParameter=4}">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
Converter
public class ColumnWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int columns;
if (int.TryParse(parameter.ToString(), out columns))
{
double width;
if (columns > 0 && double.TryParse(value.ToString(), out width))
{
return width / columns;
}
}
return 100.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
If you want to be more dynamic, you can write a Behavior and attach it to the ListView. There you can dynamic calculate the values based on the Columns count.

DataGrid Items collection doesn't always refresh

I have a DataGrid bound to an ObservableCollection<Client>.
I have a UserControl which is responsible to apply a filter on the collection from a Textbox value like this:
private void UCFilterBox_SearchTextChanged(object sender, string e)
{
var coll = CollectionViewSource.GetDefaultView(dgClients.ItemsSource);
coll.Filter = o =>
{
var c = o as Client;
if (c != null)
{
bool ret = (the filter...)
return ret;
}
else
{
return false;
}
};
}
Then I have a TextBlock which is bound to the DataGrid's Items collection like this:
<StackPanel Grid.Row="0"
Margin="215,0,0,5"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<TextBlock Style="{StaticResource SmallTextBlockStyle}" Text="{Binding ElementName=dgClients, Path=Items.Count}" />
<TextBlock Style="{StaticResource SmallTextBlockStyle}"
Text="{Binding ElementName=dgClients, Path=Items.Count, Converter={StaticResource ClientSingleOrPluralConverter}, StringFormat={} {0}}" />
</StackPanel>
This is working correctly, and each time the DataGrid is filtered, the value changes accordingly.
However, I have another TextBlock bound to the DataGrid's Items collection, which is responsible of showing the sum of the displayed data, and this one is not updating !
<TextBlock Margin="5"
FontWeight="Bold"
Text="{Binding ElementName=dgClients,
Path=Items,
Converter={StaticResource CalculateSumConvertor},
StringFormat={}{0:C}}" />
The CalculateSumConvertor is only hit once at the binding of the DataGrid and then no more.
Here is the converter:
public class CalculateSumConvertor: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var clients = value as ItemCollection;
if (clients != null)
{
return clients.Cast<Client>().Sum(c => c.FieldToSum);
}
else
{
return DependencyProperty.UnsetValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Is there something I'm doing wrongly here ?
Change your binding to the following and change your converter to an IMultiValueConverter
<TextBlock Margin="5"
FontWeight="Bold">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource CalculateSumConvertor}" StringFormat="{}{0:C}">
<Binding ElementName="dgClients" Path="Items" />
<Binding ElementName="dgClients" Path="Items.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Converter:
class CalculateSumConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var clients = values[0] as ItemCollection;
...
}
}

How do I Bind 2 controls to 1 field AND also access the 2 control values for ConvertBack?

In a DataGridTemplateColumn DataTemplate,
I want to bind 2 controls to a string field of format "[name]:[value]" i.e. the string is delimited by colon ":". I need to bind control a) to the [name] part and control b) the value part.
I have been able to successfully use an IValueConverter to split the string for display:
public class NameAndValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string rtn = "";
string[] split = value.ToString().Split(':');
if (split.Count() == 2)
{
if(parameter.ToString() == "Name")
rtn = split[0];
if(parameter.ToString() == "Value")
rtn = split[1];
}
return rtn;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("NameAndValueConverter can only be used OneWay.");
}
}
And the XAML:
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<local:NameAndValueConverter x:Key="NameAndValueConverter" />
</StackPanel.Resources>
<TextBox x:Name="namePart" Text="{Binding Path=FieldType, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Name'}" />
<TextBox x:Name="valuePart" Text="{Binding Path=FieldType, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Value'}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
But the data may be edited in the Textboxes so how can I access the 2 TextBox values in ConvertBack so that they can be joined again?
Doing this in the XAML:
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<local:NameAndValueConverter x:Key="NameAndValueConverter" />
</StackPanel.Resources>
<TextBox x:Name="namePart" Text="{Binding Path=FieldType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Name'}" />
<TextBox x:Name="valuePart" Text="{Binding Path=FieldType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource NameAndValueConverter}, ConverterParameter='Value'}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
should be enough. You need a TwoWay binding to edit from UI, and when you set UpdateSourceTrigger to PropertyChanged the TextBox will update itself automatically when the property will be modified in the ViewModel (you'd obviously need to implement INotifyPropertyChanged )

Bind IsChecked of a CheckBox to a method

Is it possible to bind the IsChecked property of a checkbox to a custom method?
I created a list of checkboxes bound to a collection of objects. I have a second collection of objects which is a subset of the first one. I'd like to bind the IsChecked porperty of the checkbox to a method that determines if the object is contained in the second list or not
EDIT:
<ListBox Height="auto" HorizontalAlignment="Stretch" Name="listBox" VerticalAlignment="Stretch" Width="auto" ItemsSource="{Binding DataSources}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Name="CheckBoxZone"
Content="{Binding Name}"
Tag="{Binding Id}"
Margin="0,5,0,0"
/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You can bind the checkbox Command property to a ICommand on your model. This means every time the check is changed the command will be invoked.
Example:
<CheckBox Name="CheckBoxZone"
Content="{Binding Name}"
Tag="{Binding Id}"
Margin="0,5,0,0"
Command={Binding CheckBoxChangedCommand}
/>
You may bind IsChecked to both the data object and the subset collection by means of a MultiBinding in conjunction with a multi-value converter that converts into a bool (or Nullable<bool> for IsChecked) value:
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource ObjectInListConverter}" Mode="OneWay">
<Binding />
<Binding Source="{StaticResource SubsetCollection}" />
</MultiBinding>
</CheckBox.IsChecked>
The converter:
class ObjectInListConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
IList subset = values[1] as IList;
Nullable<bool> result = subset.Contains(values[0]);
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In addition to Pop Catalin's answer, you will want to bind IsChecked to a property in the VM and modify that VM property when command is executed.

Resources