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}"/>
Related
Ok,
I have seen a few similar questions but have not been able to figure out this problem for the past couple days. I have two Comboboxes and I want each one to hide the selected element in the other one. For example, if I select a value in ComboBox 1 that selected item should be removed as an option in ComboBox 2.
I thought about using a command but ComboBoxes don't have commands. I have pasted below the comboboxes' XAML and ViewModel code. I would appreciate any help with this. I know the code below is wrong but I think that the logic for this should be in the setters of the bounded to ItemSource.
<ComboBox Margin="0,7,0,0"
Name="ComboBoxA"
HorizontalAlignment="Stretch"
Header="{Binding AccountHeader}"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding ChargedAccounts,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedAccount,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Uid="TargetAccountTextBox"
Name="ComboBoxB"
Margin="0,7,0,0"
HorizontalAlignment="Stretch"
Header="target account"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding TargetAccounts,
Mode=TwoWay,
namespace MoneyFox.Shared.ViewModels
{
[ImplementPropertyChanged]
public class ModifyPaymentViewModel : BaseViewModel, IDisposable
{
private readonly IDefaultManager defaultManager;
private readonly IDialogService dialogService;
private readonly IPaymentManager paymentManager;
//this token ensures that we will be notified when a message is sent.
private readonly MvxSubscriptionToken token;
private readonly IUnitOfWork unitOfWork;
// This has to be static in order to keep the value even if you leave the page to select a category.
private double amount;
private Payment selectedPayment;
public ModifyPaymentViewModel(IUnitOfWork unitOfWork,
IDialogService dialogService,
IPaymentManager paymentManager,
IDefaultManager defaultManager)
{
this.unitOfWork = unitOfWork;
this.dialogService = dialogService;
this.paymentManager = paymentManager;
this.defaultManager = defaultManager;
TargetAccounts = unitOfWork.AccountRepository.Data;
ChargedAccounts = unitOfWork.AccountRepository.Data;
token = MessageHub.Subscribe<CategorySelectedMessage>(ReceiveMessage);
}
ObservableCollection<Account> _SelectedAccount;
ObservableCollection<Account> SelectedAccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for(int i = 0; i < ChargedAccounts.Count; i++)
{
if(ChargedAccounts[i].ToString() == _SelectedAccount.ToString())
{
ChargedAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
ObservableCollection<Account> _TargetAccount;
ObservableCollection<Account> Targetccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for (int i = 0; i < TargetAccounts.Count; i++)
{
if (TargetAccounts[i].ToString() == _SelectedAccount.ToString())
{
TargetAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
While I do agree with a lot of the points in the answer provided by Ed, there is a simpler way to do this without DataTriggers or Converters. There is already a filterable CollectionViewSource in the framework that is your friend (Scott Hanselman loves it)
I would bind ComboBoxA to your regular ChargedAccounts property, but I would modify ComboBoxB to:
bind to a property in the code behind of the View that returns a ICollectionView
in a SelectionChanged event handler for ComboBoxA (also in the code behind of the view) I would adjust the filter for the ICollectionView to exclude the currently selected item
Roughly, this can be done in just a couple of lines:
public ICollectionView FilteredData { get; set; }
private void ComboBoxA_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var z = new CollectionViewSource {Source = ViewModel.ChargedAccounts.Where(p => p != ViewModel.SelectedAccount) };
FilteredData = z.View;
}
Of course this assumes you've done the right thing by having a ViewModel property in the code behind of your view preferably exposed as an interface, and that the ChargedAccounts and SelectedAccount properties are available via that interface.
You could also cobble these couple of lines together in your viewmodel and trigger it via a property change on SelectedAccount - I just have the opinion that a filter operation in response to a UI action should go in the code behind of the UI, but that decision is really up to you.
Give the comboboxes an ItemContainerStyle (TargetType="ComboBoxItem") with a data trigger. For ComboBoxA, that'll look like this:
<ComboBox
...
x:Name="ComboBoxA"
...
>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{local:ObjectEquals}"
>
<Binding
Path="SelectedItem"
ElementName="ComboBoxB" />
<!-- Binding with no properties just binds to the DataContext -->
<Binding />
</MultiBinding>
</DataTrigger.Binding>
<Setter
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
ComboBoxB gets the same deal, but ElementName="ComboBoxA" in the SelectedItem binding.
And we'll need to write that multi-value converter. It's as easy as they come:
public class ObjectEquals : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Length == 2 && values[0] == values[1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
It'd be so convenient if you could bind DataTrigger.Value to {Binding}, but it's not a dependency property.
You *could^ also do this purely in the viewmodel by temporarily removing SelectedAccount from TargetAccounts -- you'd have a private full _targetAccountsFull list, and a public filtered one. The setter for SelectedAccount would filter the list. Were you trying to do that already?
But that's not my idea of a good solution. Hiding combo box items is UI design stuff; the viewmodel shouldn't be involved in it, and in fact shouldn't even be aware that such things take place. One of the pleasures of WPF/MVVM is that you can separate that stuff out into pure UI code in the view. The viewmodel has its own complexities to worry about.
By the way, you bind SelectedItem to SelectedAccount, but SelectedAccount is an ObservableCollection. That makes no sense. There's one selected account. Make it a single Account, not a collection of them.
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.
In my WPF application (using MVVM) I have a CheckBox and a TextBlock. When the CheckBox is checked the value from the TextBlock will be saved. There is a binding from both controls to my ViewModel. Below simplified XAML:
<StackPanel>
<Label>Add to list</Label>
<CheckBox IsChecked="{Binding Path=AddItem}"></CheckBox>
<Label>Gross amount:</Label>
<TextBlock Text="{Binding Path=Amount}"></TextBlock>
</StackPanel>
Now I would like to have the CheckBox checked when a user starts typing in the TextBlock. I know binding can do that but I already bind to a property in my ViewModel. How can I bind to a property in ViewModel and to other control?
You should use the multibinding. Something like this:
<CheckBox Content="CheckBox" HorizontalAlignment="Left" Margin="191,82,0,0" VerticalAlignment="Top">
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource checkConverter}">
<Binding Path="IsChecked"/>
<Binding Path="UserStartedTyping"/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
checkConverter is a MultiValueConverter that you need in order to decide what to do with the values you are binding with (such as &&, || etc.).
public class CheckConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return (bool)((bool)values[0] || (bool)values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
object[] splitValues = { value, false };
return splitValues;
}
}
UserStartedTyping is a property in the ViewModel that would be set to true when KeyDown event is fired.
Hope it helps.
You can try setting the AddItem to true when the user starts to change the amount value:
private string _amt;
public string Amount
{
get{return _amt;}
set
{
_amt = value;
if(AddItem == false)
AddItem = true;
//PropertyChanges here
}
}
Let's say I have a ListBox which binds to stuff in code-behind:
<ListBox x:Name="list">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem Content="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox x:Name="name" Text="{Binding ElementName=list, Path=SelectedItem.Name, Mode=TwoWay" />
<TextBox x:Name="contents" Text="{Binding ElementName=list, Path=SelectedItem.Contents, Mode=TwoWay" />
Code behind:
public class Dude
{
public String Name { get; set; }
public String Contents { get; set; }
}
Now the above does just what I want it to. When an item in the listbox is selected, The textboxes update to show what was selected in the listbox.
But what I am now trying to do is expand on my Dude class by adding a Dictionary to it:
public class Dude
{
public string Name { get; set; }
public string Contents { get; set; }
public Dictionary<String, String> Tasks { get; set; }
}
In the hopes that I can:
Click an Item in the ListBox, have the corresponding item's Name and
Contents properties display in their respective TextBoxes and then
Append to the contents textbox the Key/Value of the Dictionary's
contents.
But I don't know how I can go that deep. It kinda feels like I'm going multiple levels, would something like Multidimensional binding be what I need?
Are there any (simple) samples you have, or have seen? Docs, articles, tutorials?
Any help is much appreciated.
Thank you
What you want can be done in WPF (bit harder in other XAML technologies like Wp7 or WinRT), but I'm not sure it's what you need...
Use MultiBinding, to bind both the Contents string and the Tasks dictionary to the second textbox, then write your own IMultiValueConverter to build the string you want to display.
Read the tutorial about MultiBindings here, and the rough code should look something like this:
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource YourAppendingConverter}">
<Binding ElementName="list" Path="SelectedItem.Contents" />
<Binding ElementName="list" Path="SelectedItem.Tasks" />
</MultiBinding>
</TextBox.Text>
</TextBox>
and your converter should resemble:
public class YourAppendingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture){
StringBuilder sb = new StringBuilder(values[0].ToString());
sb.AppendLine("Tasks:");
foreach (var task in (Dictionary<string,string>)values[1]){
sb.AppendLine(string.Format("{0}: {1}", task.Key, task.Value));
}
return sb.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
System.Globalization.CultureInfo culture){
throw new NotSupportedException();
}
Why do I think this is what you need?
Writing a ConvertBack if needed is hell - so this TextBox should be readonly
Dictionary is not notifying. If the underlying dictionary changes while you displaying it, nothing will change
If you think these through, then MultiBinding might be the tool you need.
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