How to use MultiBinding in a WPF ComboBox - c#

This is driving me NUTS!!!
I have a ComboBox used to filter a query by employee which works fine but only displays the employees first name. I want to use a MultiValueConverter to display the employees full name (This would be less urgent if we did not have 2 Mikes and 2 Daves)
Below is my working code and the IMultiValueConverter Class (With unnecessary formatting trimmed out for brevity). I have tried everything I can think of to get the MultiConverter to work but I have had no luck.
<ComboBox ItemsSource="{Binding Path=EmployeesFilter}"
DisplayMemberPath="EmpFirstName"
SelectedValue="{Binding Path=EmployeeToShow, Mode=TwoWay}"/>
The ViewModel Properties it is bound to:
// This collection is used to populate the Employee Filter ComboBox
private ObservableCollection<Employee> employeesFilter;
public ObservableCollection<Employee> EmployeesFilter
{
get {
return employeesFilter;
}
set {
if (employeesFilter != value)
{
employeesFilter = value;
OnPropertyChanged("EmployeesFilter");
}
}
}
// This property is TwoWay bound to the EmployeeFilters SelectedValue
private Employee employeeToShow;
public Employee EmployeeToShow
{
get {
return employeeToShow;
}
set {
if (employeeToShow != value)
{
employeeToShow = value;
OnPropertyChanged("EmployeeToShow");
QueryIssues(); // Requery with new employee filter
}
}
}
The IMultiValueConverter:
class StringsToFullNameMultiConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
// I added this because I kept getting DependecyProperty.UnsetValue
// Passed in as the program initializes
if (values[0] as string != null)
{
string firstName = (string)values[0];
string lastName = (string)values[1];
string fullName = firstName + " " + lastName;
return fullName;
}
return null;
}
public object[] ConvertBack(object value,
Type[] targetTypes,
object parameter,
System.Globalization.CultureInfo culture)
{
return null;
}
}
I tried a lot of different things but basically am using the following in the ComboBox
<ComboBox.SelectedValue>
<MultiBinding Converter="{StaticResource StringsToFullNameMultiConverter}"
Mode="OneWay" >
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName"/>
</MultiBinding>
</ComboBox.SelectedValue>
As it stands now the converter is called when the program initializes with the values set to DependencyProperty.UnsetValue. after that it is never called again, even when you select a name from the box. The names are still displayed as a first name.
Thanks for any help or pointers to good tutorials/samples you can provide. All the ones I keep finding on the web are for textboxes and I can use them just fine all day.

You're close! What you want to do though is ComboBox.ItemTemplate, not SelectedValue. Prepare for some XAML hell.
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringsToFillNameMultiConverter}">
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
Also, if I recall correctly, you don't need to create your own converter if you're just formatting strings. I think you can do the following (someone please correct me if I'm wrong.)
<!-- "Last, First" -->
<MultiBinding StringFormat="{}{1}, {0}">
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName" />
</MultiBinding>

You may be better of using a data template for the items, so you have complete control over how each person is displayed in the dropdown list.
The type convert is OK provided you don’t have a need to control the formatting of the different fields.

I ended up by addig a Readonly Property to my Class and use Displaymemberpath in the Combobox
public class MyEmployee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string DisplayName {
get { return FirstName + " " + LastName; }
}
}
Could something like this work for your situation...?
BR,
Daniel

Related

Binding dynamic list of CheckBoxes' IsChecked property

I have a situation where I load a list of objects from an SQL database. For each of the objects, I want to display a CheckBox in an ItemsControl and have its IsChecked property bound to true if and only if a member of the window's DataContext contains that object in a list.
Let's call my window MyWindow. The DataContext of MyWindow is an object of type MyContext which has a list of objects (loaded from a database) of type DataObject, and an object of type Item:
public class MyContext {
public Item CurrentItem { get; set; }
public List<DataObject> Data { get; set; }
}
public class Item {
public List<DataObject> CheckedDataObjects { get; set; }
}
In MyWindow.xaml I have my ItemsControl which is bound to the Data list. The ItemTemplate defines that each DataObject should be displayed with a CheckBox, which should have its IsChecked bound to true if and only if the particular DataObject is contained in MyWindow.DataContext.CurrentItem.CheckedDataObjects.
My best idea is to use an IMultiValueConverter approach, however I get an XamlParseException with an inner InvalidOperationException, saying that two-way binding requires Path or XPath (loosely translated). Please advise!
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox>
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource MyItemHasDataObjectConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyWindow}}" Path="DataContext.CurrentItem"/>
<Binding RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
public class ItemHasDataObjectConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
// values should contain two objects: the CurrentItem object and an object of type DataObject
if (values.Length == 2 && values[0] is Item && values[1] is DataObject) {
return (values[0] as Item).CheckedDataObjects.Contains(values[1] as DataObject);
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
return null;
}
}
EDIT
After Aabid's answer below, the converter now seems to work correctly. Further, I've added Checked and Unchecked event handlers to the CheckBox objects, which add/remove the corresponding DataObject object from CurrentItem.CheckedDataObjects. However, if I reset CurrentItem.CheckedDataObjects via code behind, either by calling Clear() or setting CheckedDataObjects = new List<DataObject>(), the CheckBox does not get updated in the UI (they stay checked).
I have made sure both MyContext and Item implement INotifyPropertyChanged and fire the corresponding OnPropertyChanged methods.
Add the Path=DataContext property to your second Binding in your multivalue binding, i.e
<Binding Path=DataContext RelativeSource="{RelativeSource Self}"/>

WPF Multibinding: OneWayToSource binding from TextBox updated via another binding doesnt work?

I have a DataGrid bound to the People collection. Also I have a TextBox that should accept the Name value from the selected row. User can then edit the value or can leave it as is. The key point is: the text shown in the TextBox no matter whether it originates from collection or user typing must be propagated to the property NewName.
I've set two bindings for the NewNameTextBox: OneWay'ed to the CollectionView behind the DataGrid, and OneWayToSource'ed to the property:
<Window.Resources>
<CollectionViewSource x:Key="PeopleCollection"
Source="{Binding Path=People, Mode=OneWay}" />
<local:ConverterNewNamePrefill x:Key="ConverterNewNamePrefill" />
</Window.Resources>
<Grid>
<StackPanel>
<DataGrid ItemsSource="{Binding Source={StaticResource PeopleCollection}}"
AutoGenerateColumns="True"
IsReadOnly="True"
Margin="10">
</DataGrid>
<StackPanel Orientation="Horizontal" Margin="10">
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource ConverterNewNamePrefill}" >
<Binding Source="{StaticResource PeopleCollection}" Path="Name" Mode="OneWay" />
<Binding Path="NewName" Mode="OneWayToSource" />
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
I suppose the property should be updated when user changes selection in the DataGrid, but this doesn't happen. The TextBox gets updated and shows the selected Name value, but the property bound via OneWayToSource remains unchanged.
If the user types into the TextBox, the property gets updated as expected.
So the question is how can I update a property from both the sources via multi-bound TextBox without code behind view?
Here is the code behind window:
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private ObservableCollection<Person> _people = new ObservableCollection<Person> {
new Person() {Name = "Mitchell", Surname = "Sofia" },
new Person() {Name="Bush", Surname="Ethan" },
new Person() {Name="Ferrero", Surname="Emma" },
new Person() {Name="Thompson", Surname="Aiden" }
};
public ObservableCollection<Person> People => _people;
public string NewName { get; set; } = "Jackson";
}
public class ConverterNewNamePrefill : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value, value };
}
}
The converter's method ConvertBack() is called only when user types, but not when the TextBox.Text updated from collection.
Thank you!
This is just how bindings work. The source or sources are not updated unless the target changes by means other than the binding itself. I.e. it's assumed that if the target was just updated from the source(s), then the source(s) is(are) already up-to-date and do not need updating.
Without more details it's difficult to know for sure what you want. But it seems like you might either want for NewName to actually be the target of a second binding, where the source is the same Name property being used as the source for the TextBox.Text property, or you want subscribe to the TextBox.TextChanged event and your handler explicitly write back the value to the NewName property when that event is raised.
In the former case, you'll have to make NewName a dependency property of MainWindow. That's a complication you may or may not want to deal with. If not, then I'd recommend the latter approach.

Apply conversion to all elements in a ComboBox drop down menu

I am trying to change the text only for contents of all items in a ComboBox based on a specific property in the ViewModel. I’ve created a DataTemplate with the Binding values as SelectedValue and the specific property I want to base the conversion on SomeProperty:
<ComboBox ItemsSource="{Binding Path=ChannelValues}"
SelectedValue="{Binding Path=Channel, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ResourceKey=ChannelNumConverter}">
<Binding Path="SelectedValue"
RelativeSource="{RelativeSource AncestorType={x:Type ComboBox}}" />
<Binding Path="DataContext.SomeProperty"
ElementName="DataContextView" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This appears to work, expect all the values in the drop down get changed to the translated SelectedValue. I’ve tried replacing SelectedValue with Text, but that doesn’t work either. Is there a way to apply this conversion to all values in the drop down (again, only changing the displayed values, not the underlying data)?
Update - ViewModel
// Populate somewhere with values
private ObservableCollection<ushort> mChannelValues = new ObservableCollection<ushort>();
public ObservableCollection<ushort> ChannelValues
{
get
{
return mChannelValues;
}
}
private ushort mChannelNum;
public ushort Channel
{
get
{
return mChannelNum;
}
set
{
if (mChannelNum != value)
{
mChannelNum = value;
OnPropertyChanged(new PropertyChangedEventArgs("Channel"));
}
}
}
private ushort mSomeProperty;
public ushort SomeProperty
{
get
{
return mSomeProperty;
}
set
{
if (mSomeProperty!= value)
{
mSomeProperty= value;
OnPropertyChanged(new PropertyChangedEventArgs("SomeProperty"));
}
}
}
Update 2 - Simple Converter
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (targetType != typeof(string))
throw new InvalidOperationException("The target must be a string");
if ((values[0] != null) && (!(values[0] is ushort)))
throw new InvalidOperationException("The channel must be an short");
if ((values[1] != null) && (!(values[1] is ushort)))
throw new InvalidOperationException("The some property must be a ushort");
ushort ushort_val = ushort.Parse((string)values[0]);
ushort ushort_some_property = ushort.Parse((string)values[1]);
switch (ushort_some_property)
{
case 0:
return (ushort_val + 1).ToString();
case 1:
return (ushort_val + 7).ToString();
case 2:
return (ushort_val + 2).ToString();
default:
return ushort_val.ToString();
}
}
Instead of using a MultiBinding, you could use SomeProperty as a ConverterParameter
<TextBlock Text="{Binding Converter={StaticResource ResourceKey=ChannelNumConverter}, ConverterParameter={Binding SomeProperty}}"/>
In converter:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var someProperty = parameter as SomeType;
...
The problem is in fact that your itemTemplate is applied to ALL! Items in the combobox, whose converter actually processes the currently selected item and someproperty leading to equal values in all items.
The approach at this point is not the problem. You just have to bind the current text's value in the viewmodel instead of the selected value.
This will combine two values from your viewmodel into the result displayed in the textbox without any recursive updates.
Finally figured out how to do this. Below is the xaml to apply the converter to all elements in the drop down list:
<ComboBox ItemsSource="{Binding Path=ChannelValues}"
SelectedValue="{Binding Path=Channel, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ResourceKey=ChannelNumConverter}">
<Binding />
<Binding Path="DataContext.SomeProperty"
RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType=local:DataContextView}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

Combining text with data

I have a string like follows.
string someInfo = string.Format("First Name = {0}, Last Name = {1}",firstName, lastName);
This string need to be displayed in application using TextBlock. The first and last names are coming from database so I would like to using data bindings for this. Is it possible to do?
Yes, its possible.
However, because you have multiple bindings, you need to bind to a MultiBinding (MSDN).
Your binding looks like:
<TextBlock.Text>
<MultiBinding Converter="{StaticResource NameConverter}">
<Binding Path="FirstName"/>
<Binding Path="LastName"/>
</MultiBinding>
</TextBlock.Text>
With a MultiValueConverter:
public class NameConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return string.Format("First Name = {0}, Last Name = {1}", values[0], values[1]);
}
public objct ConvertBack(...)
{
return Binding.DoNothing;
}
}
I don't know whether you use the MVVM pattern. if you do just define a property in your ViewModel
public string Someinfo
{
get { return string.Format("First Name = {0}, Last Name = {1}",firstName, lastName);}
}
and then use a Binding in your Xaml
<TextBlock Text={Binding Path Someinfo} />
I would say this is 'cleaner' than doing that in your xaml.
yes it possible
public string SomeInfo { get; set; }
public MainWindow()
{
InitializeComponent();
SomeInfo = GetFirstNameAndLastNameFromDataBase();
DataContext = this;
}
private string GetFirstNameAndLastNameFromDataBase()
{
string firstName = "firstName";
string lastName = "lastName";
return string.Format("First Name = {0}, Last Name = {1}", firstName, lastName);
}
<Window x:Class="BindingToTextBlock.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">
<Grid>
<TextBlock Text="{Binding SomeInfo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></TextBlock>
</Grid>
</Window>

ComboBox Validation without Binding

This question is based on this solution, but I somehow cannot get this to work using a ComboBox. I would like to validate that upon clicking a submit button, the combobox has a selected item and is not null. Please note that I am not binding to anything on purpose, and not because I don't know how to. But if the answer is that there is no way I can use the validation rules without binding (to ComboBoxes specifically, I've done it to textboxes via the linked solution), please let me know.
Here is what I have so far:
XAML:
<ComboBox DataContext="{StaticResource ChargeAssigneeViewSource}" Name="ChargeAssigneeBox" ItemsSource="{Binding}" Width="85">
<ComboBox.SelectedItem>
<Binding RelativeSource="{RelativeSource Self}" Path="GetType" Mode="TwoWay">
<Binding.ValidationRules>
<my:ComboBoxValidationRule ErrorMessage="Please select an Assignee" />
</Binding.ValidationRules>
</Binding>
</ComboBox.SelectedItem>
</ComboBox>
Validation Rule:
class ComboBoxValidationRule : ValidationRule
{
private string errorMessage;
public string ErrorMessage
{
get { return errorMessage; }
set { errorMessage = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
return new ValidationResult(false, ErrorMessage);
return new ValidationResult(true, null);
}
}
Button Click:
private void AddAnHours()
{
Employee currEmployee;
if (!ValidateElement.HasError(ChargeAssigneeBox))
{
if (!ValidateElement.HasError(analystTimeTxtBox))
{
currEmployee = ChargeAssigneeBox.SelectedItem as Employee;
item.AddTime(currEmployee, DateTime.Now, double.Parse(analystTimeTxtBox.Text));
analystTimeTxtBox.Clear();
ChargeAssigneeBox.SelectedItem = null;
}
}
UpdateTotals();
}
The error that I get is in this line:
currEmployee = ChargeAssigneeBox.SelectedItem as Employee;
but my ItemsSource is binding properly so even though I have selected an item, the selecteditem is not converting it to an employee object. I suspect it has something to do with what I am binding it to:
<Binding RelativeSoruce="{RelativeSource Self}" Path="GetType"....>
Help would be greatly appreciated, thanks.
You can't bind a property to the selectedItem on the combobox and then check if thats property is null?
Public string SelectedComboboxItem { get; set; }
<ComboBox DataContext="{StaticResource ChargeAssigneeViewSource}" SelectedItem="{Binding SelectedComboboxItem}" Name="ChargeAssigneeBox" ItemsSource="{Binding}" Width="85">

Categories

Resources