I have a ComboBox bound to a list of values, for the sake of this example, a simple array of ints. The control has a custom ItemTemplate assigned to format its elements, which, in turn, uses an IValueConverter for some of its operation.
There's a particular case where this fails: If the ComboBox has an element selected, and the list of items changes, the converter's Convert is invoked with an empty string, which is most definitely not one of the values bound to my control. (Note that I'm not talking about the ConvertBack method.)
My questions would be:
Why is the IValueConverter being invoked with value being an empty string ("") when no string is ever bound to the control?
What are some non-hacky solutions to the problem? (I could just place if (value is "") return null; in the converter, and it seems to make the error go away, but I feel like it's treating the symptoms, not the cause.)
The problem can be reproduced with a simple WPF project (dotnet new wpf) containing only these 3 files (the problem is present on both the Framework and Core versions of .NET):
MainWindow.xaml:
<Window x:Class="test.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:test"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox x:Name="Selector" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={x:Static local:Converter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Replace" Click="Button_Click" />
</Grid>
</Window>
MainWindow.xaml.cs:
using System;
using System.Windows;
namespace test
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Selector.ItemsSource = new int[] { 1, 2, 3 };
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Selector.ItemsSource = new int[] { -1, -2, -3 };
}
[STAThread]
public static void Main(String[] args)
{
new MainWindow().ShowDialog();
}
}
}
And finally Converter.cs:
using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Data;
namespace test
{
class Converter : IValueConverter
{
public static Converter Instance { get; } = new Converter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Debug.Assert(value is int);
return (2 * (int) value).ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
These 3 files form a minimal example that exhibits this behavior. If the "replace" button is clicked without selecting anything from the ComboBox first, the program runs fine. If, however, any value is selected in the ComboBox before clicking the button, the assertion in the converter fails, because upon clicking the button, the converter is passed a value of "".
I Tried your code when a currently selected item is not in the newly set item source the selected item should be changed right? So they change the selected item to "". That is the selected item you see in the initial state of the app running.
For a fact, if you keep the selected item in the new item source then it won't execute convert with value "".
Consider your initial item source is
Selector.ItemsSource = new int[] { 1, 2, 3 };
Now you select the second element which is displayed as 4 in your combo box and click replace to run this.
Selector.ItemsSource = new int[]{ -1, 2, -3 };
Then it won't execute the converter with value "".
Here the problem is the selected item is not present in the new item source so the Combobox is selecting its default value and your Combobox will behave like the exact way you started your application.
Related
This question already has answers here:
DataGrid DataGridTemplateColumn ComboBox
(1 answer)
ComboBox SelectedItem property updating all other comboboxes in my Datagrid row
(1 answer)
Closed 2 years ago.
I have a WPF application that has a Combobox that is supposed to be bound to a boolean value. The UI should have a combobox with just two choices to choose from. Here is what I have so far. The XAML:
<DataGridTemplateColumn Header="Instance" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="{x:Type dataModel:ParameterSetting}">
<Grid Visibility="{Binding IsPlaceholder, Converter={StaticResource BoolToVisInvert}}">
<ComboBox SelectedIndex="{Binding Instance, Mode=TwoWay, Converter={StaticResource ConvBoolToInstance}}">
<ComboBoxItem>Instance</ComboBoxItem>
<ComboBoxItem>Type</ComboBoxItem>
</ComboBox>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
The referenced converter:
[ValueConversion(typeof(bool), typeof(int))]
public class ConvBoolToInstance : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject())) return 0;
if (!(value is bool e))
throw new ArgumentException(#"Value was not a boolean and could not be converted", nameof(value));
return e ? 0 : 1;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject())) return true;
if (value == null || !(value is int i))
throw new ArgumentException(#"Value was not an integer and could not be converted", nameof(value));
return i == 0;
}
}
And the property that it's trying to bind to:
/// <summary>
/// If the parameter should be added as instance
/// </summary>
public bool Instance
{
get => _instance;
set
{
if (_instance == value) return;
_instance = value;
OnPropertyChanged(nameof(Instance));
}
}
When I debug this I get the correct choices in the combobox. I can change the default of the field and the combobox displays the correct item, plus I can set a breakpoint on the getter for the property and it's hit so I know that is correctly bound and going to the property I want it to. However, when I change the combobox value it changes the UI but doesn't push back to the property value on the object. I have set break points in both the property setter and the converter and neither are hit. From what I can tell it's just not pushing it back which is what it should do with a two way binding...
What am I missing?
EDIT
The converter is thanks to this answer but I have also tried biding to it as a SelectedValue with boolean combobox items but nothing seems to make it bind back...
EDIT
I should also mention this is inside a DataGrid. Updated question to show more of the XAML
Ok finally figured this out and a bit of a face palm, but posting the answer to help anyone in my same position. The answer was to use UpdateSourceTrigger=PropertyChanged. I believe it defaults to LostFocus which is fine but in this case it was REALLY delayed. Like I selected something in the ComboBox, then clicked into another cell to edit it, then clickout out of THAT cell which apparently finally took focus because THEN it committed. Not sure why it would be so delayed like that but once I switched to PropertyChanged it worked like a charm.
Here is my final binding (after figuring this out I was also able to just use normal binding without the converter):
SelectedItem="{Binding Instance, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
I have a Datagrid in which data comes from Database based on some selection and therefore the values keep on changing at different selections.
On the basis of the data, I want to add a TextBox to the UI?
This is .NET framework 4.5
<DataGrid Name="Sampledatagrid" AutoGenerateColumns="True" ItemsSource="`{StaticResource MyCustomers}" Margin="0,0,0,106"/>
<Canvas>
<br/>
<!-- if Sampledatagrid.Value == 'Adam' -->
<br/>
<br/>
<TextBox Canvas.Left="135" Canvas.Top="12" Style={StaticResource textboxstyle} />
<br/>
<br/>
</Canvas>
When the Sampledatagrid contains "Adam" it should display a textbox.
First, to let your TextBox show the selected entry of the DataGrid, set its Text-Binding to the corresponding value.
Just add the 'Text' property in the correct way:
<TextBox Text="{Binding ElementName=Sampledatagrid, Path=SelectedItem.Name}" Canvas.Left="135" Canvas.Top="12">
Possibly, the name of your variable "SelectedItem.Name" is another one than "Name".
Second, if the TextBox shall only show 'valid' names (like e.g. 'Adam'), I show you how to use a converter to do so.
Create a new file in your project called 'ValidNamesConverter.cs'. This file gets following code:
using System;
using System.Globalization;
using System.Windows.Data;
namespace WpfApp1
{
internal class ValidNamesConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string valAsString = value.ToString();
if (valAsString == "Adam" || valAsString == "Eve")
return value;
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
In the "Resources" section of your MainWindow.xaml (or UserControl.xaml) add an instance (a static resource) of this converter-class. (if you don't have already a "Resources" section, just add one):
<Window.Resources>
<!-- make a XAML instance of the class ValidNamesConverter -->
<local:ValidNamesConverter x:Key="validNamesConverter" />
</Window.Resources>
Then, extend the TextBox's Text-Binding to use this converter.
<TextBox Text="{Binding ElementName=Sampledatagrid, Path=SelectedItem.Name, Converter={StaticResource validNamesConverter}}" Canvas.Left="135" Canvas.Top="42">
The converter in this case decides, which string shall be 'forwarded' to the TextBox. So, if a valid name is given into the converter, it will just return that name again. If an invalid name is given, the converter returns an empty string.
Anyway, in my example, the TextBox is always visibly shown. If you want the TextBox to be absent when an invalid name is selected, you probably need a second converter (e.g. ValidNameToVisibilityConverter) which you bind to the TextBox's Visibility-property. That converter then returns either Visibility.Visible or Visibility.Collapsed. Within this 2nd converter you can make use of the ValidNamesConverter to avoid having a set of valid names twice in your code.
I have a WPF window that only has a ComboBox (drop down list). If I choose index 1 (second item on the drop down list), how can I extend that WPF window to show more buttons, textbox, etc? Would I need to use the selectedIndex property? If so how do I make the window extend in the XAML.
I have used a IValueConverter in the past to accomplish this. Here is a sample converter:
public class MyConverter : System.Windows.Data.IValueConverter {
public object Convert ( object value , Type targetType , object parameter , CultureInfo culture ) {
if ( value == null )
return System.Windows.Visibility.Hidden;
if ( parameter == null )
return System.Windows.Visibility.Hidden;
if ( value.ToString().Equals ( parameter ) )
return System.Windows.Visibility.Visible;
return System.Windows.Visibility.Hidden;
}
public object ConvertBack ( object value , Type targetType , object parameter , CultureInfo culture ) {
throw new NotImplementedException ( );
}
}
What this does is, it takes the value that is passed to it, I am expecting a number such as the SelectedIndex of an items control. I then compare it to the parameter that is passed. If they are equal, I return Visibility.Visible. In all other instances, I return Visibility.Hidden.
Now, you can take that and plug it into the XAML like this:
<Window x:Class="WpfApplication1.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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:MyConverter x:Key="vConv"/>
</Window.Resources>
<Grid>
<ComboBox x:Name="comboBox" HorizontalAlignment="Left" Margin="25,52,0,0" VerticalAlignment="Top" Width="120">
<ComboBoxItem>Hidden</ComboBoxItem>
<ComboBoxItem>Visible</ComboBoxItem>
</ComboBox>
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" Margin="219,92,0,0" VerticalAlignment="Top" Visibility="{Binding ElementName=comboBox, Path=SelectedIndex, Converter={StaticResource vConv}, ConverterParameter=1, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
You can see that we created an instance of our MyConverter class in the Window.Resources. When we use this in our binding, we can show/hide my label based on whatever index is selected. Now this is very basic and you can add a lot to this to get all the functionality you need, bu this should get you started.
You can use the MaxHeight and MaxWidth property of the window on selection of the comboBox Item. Like this:
On Selection Change event of combobox.Use this
MainWindow obj= new MainWindow();
if(mycombobox.SelectedIndex==0)
{
obj.MaxWidth="600";
obj.MinWidth="600";
}
if(mycombobox.SelectedIndex==1)
{
obj.MaxWidth="200";
obj.MinWidth="200";
}
or you also can do this
if(mycombobox.SelectedIndex==0)
{
this.MaxWidth="600";
this.MinWidth="600";
}
if(mycombobox.SelectedIndex==1)
{
this.MaxWidth="200";
this.MinWidth="200";
}
I have a ComboBox whose properties ItemsSource and SelectedValue are bound to a model. Sometimes, the model needs to adjust the selected item to a different one, but when I do it in the model, the model value is not being reflected in the View, even though the SelectedValue is properly set (checked both with snoop and in SelectionChanged event handler).
To illustrate the problem, here is a simple xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<ComboBox Height="25" Width="120" SelectedValue="{Binding SelectedValue}" SelectedValuePath="Key" ItemsSource="{Binding PossibleValues}" DisplayMemberPath="Value"/>
</Grid>
</Window>
And here is the model:
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
namespace WpfApplication1
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
int m_selectedValue = 2;
Dictionary<int, string> m_possibleValues = new Dictionary<int, string>() { { 1, "one" }, { 2, "two" }, { 3, "three" }, {4,"four"} };
public int SelectedValue
{
get { return m_selectedValue; }
set
{
if (value == 3)
{
m_selectedValue = 1;
}
else
{
m_selectedValue = value;
}
PropertyChanged(this, new PropertyChangedEventArgs("SelectedValue"));
}
}
public Dictionary<int, string> PossibleValues
{
get { return m_possibleValues; }
set { m_possibleValues = value; }
}
public MainWindow()
{
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
I Expected the behaviour to be as follows:
Initially, two is selected
Select "one" -> ComboBox displays "one"
Select "two" -> ComboBox displays "two"
Select "three" -> ComboBox displays "one"
Select "four" -> ComboBox displays "four"
However, in #4, "three" is displayed. Why? The value in the model was changed to 1 ("one"), but the View still displays 3 ("three").
I have found a workaround by explicitly updating the binding target in a SelectionChanged event handler, but this seems wrong. Is there another way to achieve this?
When you select an item, the binding engine will update the model, and will ignore any PropertyChanged for the property it just changed during the time it's invoking the property setter. If it didn't, things could potentially go into an infinite loop.
The system assumes that the setter won't change the value it was passed. The workaround here is simple: use Dispatcher.BeginInvoke() or set IsAsync=True on your binding to get the behavior you're looking for. However, I personnaly strongly discourage you to do this. Not only because it introduces a whole bunch of new problems related to timings, but mainly because it's a workaround to a problem that shouldn't exist in the first place.
Simply don't do this. It's not only applying to WPF: anything changing a setter could expect that the value it just set was correctly set is no exception is thrown. Changing the value inside the setter is counter intuitive and could bite you later. Plus, it can be really disturbing for the user of your application to see the combobox ignoring its selection.
If you don't like the value being passed, throw an exception and use Binding.ValidatesOnExceptions to tell WPF that it's expected. If you don't want the user to select this value, don't put it inside the list in the first place. If the item shouldn't be available conditionally because of other business rules, filter the list dynamically or apply triggers.
I have had persistent problems with proper updating of the control when binding to the SelectedValue and/or SelectedItem properties of a ComboBox control.
To solve this, I had to bind to the SelectedIndex property instead. It was more hassle to deal with an 'index' property in my ViewModel, but it solved the problem with the ComboBox not updating.
How to change the value of a text block inside a grid view in WPF based on the check box selection . Grid view in WPF is populated from a sql table which has ID and Value as columns.Value here is YES or NO.I am using linq to sql .
I have a check box associated to each ID in the grid view.when a user selects some rows ,i have to save the changes back to the database.
So based on the selection i have to change the value field in the row in this fashion:
If the text in the "Value" field of the grid view is "YES" then i have to change it to "NO"
If the text in the "Value" field of the grid view is "NO" then i have to change it to "YES"
I am able to populate data into the gridview ,but i am not sure whether my questions in the above scenario will fit in WPF and c#.Need some guidance.
The best way to do this is to bind both the Text block and the checkbox to the same backend field in the data model and then to use code converters.
Here is a simple example.
Say you have the following simple view model with one bool property:
class SimpleViewModel: INotifyPropertyChanged
{
private bool _checked;
// The property to bind to
public bool Checked
{
get { return _checked; }
set { _checked = value; OnPropertyChanged("Checked"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Here is also a simple page with a text block and a text field that both bind to the same backend field.
<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">
<Window.Resources>
<local:SimpleViewModel x:Key="simpleViewModel" />
<local:BoolToStringConverter x:Key="boolToStringConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource simpleViewModel}">
<StackPanel>
<TextBlock Text="{Binding Checked, Converter={StaticResource boolToStringConverter}}" />
<CheckBox Content="something" IsChecked="{Binding Checked}" />
</StackPanel>
</Grid>
</Window>
Now notice that the text block binding statement contains a converter statement.
Text="{Binding Checked, Converter={StaticResource boolToStringConverter}}"
The converter here is very simple. It checks the value if it's true and returns Yes, otherwise returns NO.
public class BoolToStringConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return null;
if ((bool)value == true)
return "YES";
else
return "NO";
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// this scenario is not needed since the text block is read only
throw new NotImplementedException();
}
}
You need to do events. Click on the control and click the lightning bold and do it in the code behind in c#. The keyword is events. OnChanged, Onclicked, onrowchange, etc. is inside that properties box for that control and you change the value in the code.
Use two way binding to transfer changes from UI into database. Bind the checkboxes column to the Value field from SQL table. You will need a convertor for binding to transform from Yes/No into bool.
http://msdn.microsoft.com/en-us/magazine/cc163299.aspx#S3
http://msdn.microsoft.com/en-us/library/ms752347.aspx#data_conversion