I bind combobox (that is a part if listbox item template) to enum, the selected item is bound to the collection that is bound to listbox.
I use a converter for some logic.
The problem is that the ConvertBack is not invoked on startup, but only when I re-select the item in combobox.
I need it to invoke also on start.
public enum FullEnum
{
Apple,
Banana,
Pear
}
<Window.Resources>
<local:EnumConverter x:Key="enumConverter"/>
<ObjectDataProvider x:Key="DataT"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:FullEnum" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="190*" />
<RowDefinition Height="71*" />
</Grid.RowDefinitions>
<ListBox Name="list1" Margin="0,0,0,37">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Label}"></TextBlock>
<ComboBox Height="23" Width="90"
ItemsSource="{Binding Source={StaticResource DataT}}"
SelectedValue="{Binding Path=Oped, Converter={StaticResource enumConverter}}">
</ComboBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
List<Item1> list = new List<Item1>();
public Window1()
{
InitializeComponent();
list.Add(new Item1 { Label="label1" });
list.Add(new Item1 { Label = "label2" });
list.Add(new Item1 { Label = "label3" });
list1.ItemsSource = list;
}
public class Item1
{
public FullEnum Oped { get; set; }
public string Label { get; set; }
}
public class EnumConverterr : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
//some code
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((int)value != 0)
return (EnumSuperior)value;
return (EnumSuperior)7;
}
}
The return converter is not called by WPF on initialization because it has just gotten the initial values from the data context. The source and target of the data binding should have the same values so there is no reason to update the source.
You have not posted your convert back logic, but you must have some "state-ful" logic in the converter. Converters should be stateless (no side-effects, immutable). All of the conversion should be based on the value, a parameter, and converter properties that are not modified during the conversion.
If your converter is stateless, all you need to do is initialize the data source properly and you should no longer need that initial convert back call.
Related
I have the following class hierarchy:
namespace WpfBindingProblem
{
public class Top
{
public IList<Mid> MidList { get; } = new ObservableCollection<Mid>();
}
public class Mid
{
public IList<Bot> BotList { get; } = new ObservableCollection<Bot>();
}
public class Bot
{
}
}
And I have this XAML window:
<Window x:Class="WpfBindingProblem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfBindingProblem"
mc:Ignorable="d"
Title="MainWindow" Height="217.267" Width="333.686">
<Window.DataContext>
<local:Top/>
</Window.DataContext>
<Window.Resources>
<local:TriggersToString x:Key="TriggersToString"/>
</Window.Resources>
<Grid>
<ListView Margin="10" ItemsSource="{Binding MidList}" x:Name="ThatList">
<ListView.Resources>
<DataTemplate DataType="{x:Type local:Mid}">
<TextBlock Text="{Binding BotList, Converter={StaticResource TriggersToString}}" />
</DataTemplate>
</ListView.Resources>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add mid" Click="AddMid"/>
<MenuItem Header="Add bot to selected mid" Click="AddBot" />
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
With these handlers:
namespace WpfBindingProblem
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AddMid(object sender, RoutedEventArgs e)
{
if(DataContext is Top p)
{
p.MidList.Add(new Mid());
}
}
private void AddBot(object sender, RoutedEventArgs e)
{
if(ThatList.SelectedItem is Mid c)
{
c.BotList.Add(new Bot());
}
}
}
}
And this converter (as a stand-in for any arbitrary converter):
namespace WpfBindingProblem
{
[ValueConversion(typeof(IList<Bot>), typeof(string))]
public class TriggersToString : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is IList<Bot> list)
{
return list.Count.ToString();
}
throw new InvalidOperationException();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
}
In the window that appears when we run this example, I can right click and choose "Add mid" so an instance of Mid is added to the Top data context, and the list view is updated accordingly, showing the number 0 (as per the conversion logic)
However, when I click "Add bot to selected mid", an instance of Bot is added to the selected Mid (I can verify this using breakpoints), but the list view is not updated accordingly (I expected 0 to be changed to 1, but the converter is not called again for that particular instance of Mid).
Why does this change not trigger an update of the GUI?
I know I can work around this with some hacks (like setting the data context to null and back, or possibly by invoking explicit updates using dependency properties), but there are two reasons why I'd like to avoid that:
My actual code is more complex than this MCVE and it would look very ugly.
I've already sprinkled all my (actual) classes with all the required the ObservableCollections and the INotifyPropertyChanged interfaces, precisely so that I wouldn't need to perform manual updates — so I feel like automatic updates should happen in this case, unless I've missed something.
Why does this change not trigger an update of the GUI?
Because the source property of the binding (BotList) is not updated. The converter is invoked only when the data bound property is updated.
You could use a MultiBinding as suggested by #Sinatr or you could
bind directly to the Count property of the collection:
<TextBlock Text="{Binding BotList.Count}" />
implement the INotifyPropertyChanged interface in the Mid class and raise the PropertyChanged event for the BotList property whenever an item is added to it. Handle CollectionChanged.
You might also move your convert logic to the view model, bind to a property of this one and also raise the PropertyChanged for it whenever you want the binding to be refreshed.
You can use multi binding:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TriggersToString}">
<Binding Path="BotList" />
<Binding Path="BotList.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
and multi value converter:
public class TriggersToString : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
(values[0] as IList<Bot>)?.Count.ToString(); // first binding
...
}
This way the converter is called whenever either of bindings is updated.
Using a wpf ListBox I'm trying to display a list of filename without displaying the full path (more convenient for user).
Data comes from an ObservableCollection which is filled using Dialog.
private ObservableCollection<string> _VidFileDisplay = new ObservableCollection<string>(new[] {""});
public ObservableCollection<string> VidFileDisplay
{
get { return _VidFileDisplay; }
set { _VidFileDisplay = value; }
}
In the end I want to select some items and get back the full file path. For this I have a converter :
public class PathToFilenameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
//return Path.GetFileName(value.ToString());
string result = null;
if (value != null)
{
var path = value.ToString();
if (string.IsNullOrWhiteSpace(path) == false)
result = Path.GetFileName(path);
}
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
Which I bind to my listbox itemsource :
<ListBox x:Name="VideoFileList" Margin="0" Grid.Row="1" Grid.RowSpan="5" Template="{DynamicResource BaseListBoxControlStyle}" ItemContainerStyle="{DynamicResource BaseListBoxItemStyle}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ItemsSource="{Binding Path=DataContext.VidFileDisplay, Converter={StaticResource PathToFileName},ElementName=Ch_Parameters, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Path=SelectedVidNames,ElementName=Ch_Parameters, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
Without the converter, it is working fine (but of course this is the full path displayed in the listbox). With the converter I have one character per line... displaying this :
System.Collections.ObjectModel.ObservableCollection`1[System.String]
Where am I wrong ?
Thank you
In ItemsSource binding converter applies to the whole list and not to each item in the collection. If you want to apply your converter per item you need to do it ItemTemplate
<ListBox x:Name="VideoFileList" ItemsSource="{Binding Path=DataContext.VidFileDisplay, ElementName=Ch_Parameters}" ...>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource PathToFileName}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I am wondering if it is possible to use two dictionaries in a listbox data template. I want to use the value of Names in a textbox, and the value of EnabledNames for a check box. Is this possible? If so, how would I go about doing it?
Some example data would be:
Dictionary<string, string> Names = new Dictionary<string, string>()
{
{ "4EG25","John" },
{"923OT", "Joe" }
};
Dictionary<string, bool> EnabledNames = new Dictionary<string, bool>()
{
{ "4EG25",false },
{"923OT", true}
};
And how I want to use in a way like:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="359" VerticalAlignment="Top" Width="673" Margin="0,0,-0.333,-0.333" ItemsSource="{Binding Path=Names, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=EnabledNames[ItemsSource.Key].Value}" />
<TextBlock Text="{Binding Path=ItemsSource.Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Just create a class that contains both values and use it as the ItemsSource
class Name
{
public string Value { get; set; }
public bool Enabled { get; set; }
}
public IEnumerable<Name> TheNames
{
get { return Names.Select(n => new Name {Value = n.Value, Enabled = EnabledNames[n.Key]}); }
}
<ListBox ItemsSource="{Binding Path=TheNames, Mode=OneWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=Enabled}" />
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This is very hard, so I'd recommend you to create specific class as DataContext for your ListBox.
What you could try in your case:
WPF binding can be done only to properties (your dictionaries are now Fields, so your bindings won't work. Your ViewModel could be something like this:
public class ViewModel
{
public ViewModel()
{
Names = new Dictionary<string, string>()
{
{ "4EG25","John" },
{"923OT", "Joe" }
};
EnabledNames = new Dictionary<string, bool>()
{
{ "4EG25",false },
{"923OT", true}
};
}
public Dictionary<string, string> Names { get; set; }
public Dictionary<string, bool> EnabledNames { get; set; }
}
In your xaml when you set DataTemplate, its DataContext is set to single entry of your ItemsSource. In your case this is KeyValuePair. Also you could use MultiBinding to bind to your EnabledNames dictionary and use converter to convert your Key to bool Value from EnabledNames:
<Window x:Class="Test31641637.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Test31641637"
Title="MainWindow" Height="350" Width="525"
Name="mw">
<Window.Resources>
<ResourceDictionary>
<local:MultiDictionaryConverter x:Key="multiDictionaryConverter" />
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<ListBox x:Name="listBox" ItemsSource="{Binding Names}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox >
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource multiDictionaryConverter}" ConverterParameter="">
<Binding Path="Key" Mode="OneWay"/>
<Binding ElementName="mw" Path="DataContext.EnabledNames"/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
<TextBlock Text="{Binding Path=Value}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
And IMultiValueConverter:
public class MultiDictionaryConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values.Length == 2 && values[0] is string && values[1] is Dictionary<string, bool>)
{/*
((Dictionary<string, bool>)values[1])[values[0] as string] =
!((Dictionary<string, bool>)values[1])[values[0] as string];*/
return ((Dictionary<string, bool>)values[1])[values[0] as string];
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
I suppose this is the easiest way to do it (but it is not easy) and it won't work, because when you click on CheckBox in your ListBox, ConvertBack method will be called, but it is impossible to convert back boolean value. So, again, the easiest way is to create specific class that represents single line of your ListBox:
public class Person
{
public string ID { get; set; }
public string Name { get; set; }
public bool IsEnabled { get; set; }
}
I'm going crazy with converters. I know that I must use it to change the "exit value" of my values, when needed, but I don't know how to use right for my case.
I have my simple MVVM (3 fields only) and my main window with a list of my items. The first item is calculated depending on a function, and can show YES or NOT, the other values are binded directly.
This is working well, but I need to change the background and foreground colors depending on the YES or NOT value I have in the first calculated field. For example:
YES (must be blue) - ITEM 1
NO (must be grey) - ITEM 2
YES (must be blue) - ITEM 3
While the internal values in my database are (in this case the calc is modulus):
2 - ITEM 1
3 - ITEM 2
4 - ITEM 3
My ListBox code is this:
<phone:PhoneApplicationPage
x:Class="Pasti.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="My App" Style="{StaticResource PhoneTextNormalStyle}" />
<TextBlock Text="My List" Style="{StaticResource PhoneTextTitle1Style}" />
</StackPanel>
<ListBox x:Name="lstPills" Grid.Row="1" ItemsSource="{Binding AllItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" Width="440">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Background="HERE MUST GO THE CONVERTER, I SUPOSE">
<TextBlock Text="{Binding IsPair, Mode=TwoWay}"/>
</Border>
<TextBlock
Text="{Binding Name}"
FontSize="{StaticResource PhoneFontSizeLarge}"
Grid.Column="1"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</phone:PhoneApplicationPage>
And the CS code is this for this page:
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// Set the page DataContext property to the ViewModel.
this.DataContext = App.ViewModel;
}
}
For the calculated field, I added this to the Model (_myNumber holds the value I must check):
// Define a custom field based on some database values
// Get is calculated, while set will force it to refresh by Notifying
public string IsPair
{
get
{
return _myNumber % 2 == 0 ? "YES" : "NO";
}
set
{
NotifyPropertyChanged("IsPair");
}
}
NOTE: Because I don't know other way to force the list to refresh, I put the set property to only notify and the TwoWay Mode, and I just do a IsPair = "" when I want it to recalculate. If there are other way to do it, will be welcome.
So, with this info, how can I made a Converter that, based on my IsPair value, set the Background property of the Border to Blue or Grey? I saw a lot of Converter examples, but still don't get the point to do exactly this.
I suppose I must put something like this in the MainPage.cs, under the MainPage Class:
// Converter for the YES-NO column on the list
public class IsPairConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (MY_CALCULATED_VALUE == "YES")
return "Blue";
return "Grey";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
But how to get the MY_CALCULATED_VALUE, and how to set the converter in the Background value of the Border?
So close!
First, bind the background to IsPair and use the converter:
<Border Background="{Binding IsPair, Converter={StaticResource IsPairConverter}}">
<TextBlock Text="{Binding IsPair, Mode=TwoWay}"/>
</Border>
In your converter, create a brush depending on the value:
public class IsPairConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// You might want to add additional checks for type safety
var calculatedValue = (string)value;
var color = calculatedValue == "YES" ? Colors.Blue : Colors.Gray;
return new SolidColorBrush { Color = color };
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And you're done.
If you want the value to be computed only one time, instead of every time IsPair is called, you can do the computation in the setter of MyNumber and assign it to IsPair:
private int myNumber;
public string IsPair { get; protected set; }
protected int MyNumber
{
get
{
return this.myNumber;
}
set
{
this.myNumber = value;
this.IsPair = value % 2 == 0 ? "YES" : "NO";
this.NotifyPropertyChanged("IsPair");
}
}
I like design time data, especially when creating small widgets. For this very simple use case I'm having trouble binding to the properties of a design-time list which I have created in xaml.
Please find my ViewModel, View and SampleData below;
ViewModel
internal class SummaryViewModel : ViewModelBase
{
public string Title { get; set; }
public IList<Person> PersonList { get; set; }
internal SummaryViewModel()
{
PersonList = new List<Person>();
}
}
Sample Data
<ViewModel:SummaryViewModel xmlns:ViewModel="ViewModel" Title="Test Title">
<ViewModel:SummaryViewModel.Connections>
<ViewModel:ConnectionViewModel Id="0" />
<ViewModel:ConnectionViewModel Id="1" />
</ViewModel:SummaryViewModel.Connections>
</ViewModel:SummaryViewModel>
View
<StackPanel x:Class="View.SummaryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="100"
d:DesignWidth="100"
d:DataContext="{d:DesignData Source=/DesignData/SampleSummaryViewModel.xaml}"
Orientation="Vertical"
Background="LightGreen">
<!-- This Works -->
<Label FontSize="10" FontWeight="Bold" Content="{Binding Title}" />
<!-- This Works -->
<ListBox ItemsSource="{Binding PersonList}" />
<!-- This DOESN'T work -->
<Label FontSize="8" Content="{Binding PersonList, Path=Count}"/>
</StackPanel>
How would you configure SampleData such that you could bind to the Count of a list specified therein?
I have tried setting the resource type as both DesignData and DesignDataWithDesignTimeCreatableTypes with no luck.
It should be:
<Label FontSize="8" Content="{Binding Path=PersonList.Count}"/>
Also Mårten is correct, you should use an ObservableCollection instead.
HTH
CityView, just as a side note: to debug DataBinding I usually use an empty converter which only returns the value it was given. I put a breakpoint in there and that way I can see what exactly is going back and forth.
public class BindTestConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
}
Combined with that and what the Output window tells me usually leads me to a solution to the problem at hand.
It should work, but become a one-time binding since your list does not implement INotifyPropertyChanged and therefore the binding is not updated when Count changes.
Try using an ObservableCollection<Person> instead.