WPF ComboBox binding behaviour - wpf

I have the following XAML markup:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
SelectedValuePath="ProductNumber" />
My View's DataContext is bound to a viewmodel containing a public property called SelectedCustomer. Customer objects contain a FavouriteProduct property of type Product and Product objects contain public properties ProductNumber and ProductName.
The behaviour I'm looking for is to have the SelectedItem of the ComboBox update the Text in the TextBox and vice versa. ComboBox to TextBox works just fine. Selecting any product in the ComboBox updates the TextBox with the product number of that product. However when I try to go the other way I get som strange behaviour. It only works for the items that come before the selected item. I will try to explain:
Consider the following list of products ([Product Number], [Product Name]):
Fanta
Pepsi
Coca Cola
Sprite
Water
Now lets say that the SelectedCustomer's favourite product is Coca Cola (must be a developer). So when the window opens the TextBox reads 3 and the ComboBox reads Coca Cola. Lovely. Now lets change the product number in the TextBox to 2. The ComboBox updates it's value to Pepsi. Now try to change the product number in the TextBox to anything higher then the number for Coca Cola (3). Not so lovely. Selecting either 4 (Sprite) or 5 (Water) makes the ComboBox revert back to Coca Cola. So the behaviour seems to be that anything below the item that you open the window width from the list in the ItemSource does not work. Set it to 1 (Fanta) and none of the others work. Set it to 5 (Water) and they all work. Could this have to do with some initialisation for the ComboBox? Potential bug? Curious if anyone else have seen this behaviour.
UPDATE:
After reading Mike Brown's response I have created properties for SelectedProduct and SelectedProductNumber. The problem I am having with this is that as soon as you select something from the ComboBox you end up in an endless loop where the properties keep updatign each other. Have I implemented the OnPropertyChanged handler incorrectly or is there something I am missing? Here is a snippet of code from my ViewModel:
private int _SelectedProductNumber = -1;
public int SelectedProductNumber
{
get
{
if (_SelectedProductNumber == -1 && SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
OnPropertyChanged("SelectedProductNumber");
_SelectedProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (_SelectedProduct == null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
OnPropertyChanged("SelectedProduct");
_SelectedProductNumber = value.ProductNumber;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
UPDATE 2
I have changed the implementation slightly now by updating the SelectedCustomer.FavouriteProduct from both properties and then using that when reading their values. This now works but I'm not sure it's the 'correct way'.
private int _SelectedProductNumber = 0;
public int SelectedProductNumber
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
SelectedCustomer.FavouriteProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
OnPropertyChanged("SelectedProductNumber");
OnPropertyChanged("SelectedProduct");
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
SelectedCustomer.FavouriteProduct = value;
OnPropertyChanged("SelectedProduct");
OnPropertyChanged("SelectedProductNumber");
}
}

Your aim is not too clear so I have written the folloiwng so support either options I can see.
To keep two elements bound to one item in sync you can set the IsSynchronizedWithCurrentItem="True" on your combobox as shown below:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
IsSynchronizedWithCurrentItem="True"
SelectedValuePath="ProductNumber" />
This will mean everything in the current window bound to the same background object will keep in sync and not give the odd behaviours you are seeing.
This quote form this longer MSDN article describes the effect:
The IsSynchronizedWithCurrentItem
attribute is important in that, when
the selection changes, that is what
changes the "current item" as far as
the window is concerned. This tells
the WPF engine that this object is
going to be used to change the current
item. Without this attribute, the
current item in the DataContext won't
change, and therefore your text boxes
will assume that it is still on the
first item in the list.
Then setting the Mode=TwoWay as suggested by the other answer will only ensure that both when you update the textbox the underlying object will be updated and when you update the object the textbox is updated.
This makes the textbox edit the selected items text and not select the item in the combolist with the matching text (which is the alternative think you are may be trying to achieve?)
To achieve the synchronised selection effect it may be worth setting IsEditable="True" on the combobox to allow users to type items in and dropping the text box. Alternatively if you need two boxes replace the textbox with a second combobox with IsSynchronizedWithCurrentItem="True" and IsEditable="True" then a styled to make it like a text box.

What you want to do is expose separate properties on your ViewModel for the currently selected product and currently selected product number. When the selected product is changed, update the product number and vice versa. So your viewmodel should look something like this
public class MyViewModel:INotifyPropertyChanged
{
private Product _SelectedProduct;
public Product SelectedProduct
{
get { return _SelectedProduct; }
set
{
_SelectedProduct = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
_SelectedProductID = _SelectedProduct.ID;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
}
}
private int _SelectedProductID;
public int SelectedProductID
{
get { return _SelectedProductID; }
set
{
_SelectedProductID = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
_SelectedProduct = _AvailableProducts.FirstOrDefault(p => p.ID == value);
PropertyChanged(this,new PropertyChangedEventArgs("SelectedProduct"));
}
}
private IEnumerable<Product> _AvailableProducts = GetAvailableProducts();
private static IEnumerable<Product> GetAvailableProducts()
{
return new List<Product>
{
new Product{ID=1, ProductName = "Coke"},
new Product{ID = 2, ProductName="Sprite"},
new Product{ID = 3, ProductName = "Vault"},
new Product{ID=4, ProductName = "Barq's"}
};
}
public IEnumerable<Product> AvailableProducts
{
get { return _AvailableProducts; }
}
private Customer _SelectedCustomer;
public Customer SelectedCustomer
{
get { return _SelectedCustomer; }
set
{
_SelectedCustomer = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer"));
SelectedProduct = value.FavoriteProduct;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So now your XAML binds to the appropriate properties and the viewModel is responsible for syncrhronization
<TextBox
x:Name="MyTextBox"
Text="{Binding Path=SelectedProductID, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
x:Name="MyComboBox"
ItemsSource="{Binding AvailableProducts}"
DisplayMemberPath="ProductName"
SelectedItem="{Binding SelectedProduct}" />
Don't forget to implement the rest of INotifyPropertyChanged and the GetAvailableProducts function. Also there may be some errors. I hand typed this here instead of using VS but you should get the general idea.

Try:
SelectedItem="{Binding Path=YourPath, Mode=TwoWay"}
instead of setting SelectedValue and SelectedValuePath.
Might work with SelectedValue too, don't forget the Mode=TwoWay, since this isn't the default.
A good approuch would to use the master detail pattern - bind the master (the items view, e.g. combobox) to the data source collection and the detail view (e.g. text box) to the selected item in the source collection, using a binding converter to read/write the appropriate property.
Here is an example:
http://blogs.microsoft.co.il/blogs/tomershamam/archive/2008/03/28/63397.aspx
Notice the master binding is of the form {Binding} or {Binding SourceCollection} and the details binding is of the form {Binding } or {Binding SourceCollection}.
To get this working you need to wrap you collection with an object that keeps the selected item. WPF has one of these built-in: ObjectDataProvider.
Example:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/068977c9-95a8-4b4a-9d38-b0cc36d06446

Related

Bind a property based on a dictionary key?

This seems to be a tricky one.
You have a data object which you bind to a number of user controls.
Simplified:
public class Shift : INotifyPropertyChanged
{
private int _baseColor;
public int BaseColor
{
get { return _baseColor; }
set
{
_baseColor = value;
OnPropertyChanged("BaseColor");
}
}
private Employees_E _employee;
public Employees_E Employee
{
get
{
return _employee;
}
set
{
_employee = value;
OnPropertyChanged("Employee");
}
}
And here is the deal:
Depending on the Employee, the user control will change its background. One way to solve that (which works fine) is of course to use a converter, like...
Background="{Binding Employee, ElementName=uc, Converter={StaticResource EmployeeValueConverter}
But when all of it is dynamic it complicates things a lot. I don't know in advance the number of, or the name of the Employee, or the associated color to the employee.
What I want is my user control to bind to a Dictionary so that Shift.BaseColor binds to the value where the key is Employee. Something like:
Background="{Binding BaseColor, ElementName=anynamespace, Converter={StaticResource AnotherConverter}
Or preferable:
Background="{Binding MyDictionary[Employee].Value... with a converter...
The user will be able to change the associated color without changing the data object so I need another way to update my user controls when the color change.
The value will be an integer and in the converter I return a LinearGradientBrush form a list, so the integer will be an index in that list.
UPDATE 1
The background i will change is a Border background where Multibinding isn't possible? I have found another thread showing how to do it with multibinding but doesn't work in this case...?
UPDATE 2
The problem isn't how to get access to the values or the converter. The problem is when the user change the value in the dictionary and how to properly bind that to make the user control update it's background. Let's say we have a key=MARIA and a value of 21 which is the index of my List. Then the user control has a certain color. But the user might want to associate another color to that key, but there I can't figure out how to bind it.
<UserControl x:Class="SPAS.Controls.ShiftControl" ...
<Border BorderThickness="1" x:Name="myBorder" Background="{Binding BaseColor, ElementName=uc, Converter={StaticResource BaseColorLinear}, Mode=TwoWay}" >
...
</Border>
</UserControl>
public partial class ShiftControl : UserControl
{
public static DependencyProperty BaseColorProperty = DependencyProperty.Register("BaseColor", typeof(int), typeof(ShiftControl), new UIPropertyMetadata(0));
public int BaseColor
{
get { return (int)GetValue(BaseColorProperty); }
set { SetValue(BaseColorProperty, value); }
}
public static DependencyProperty EmployeeProperty = DependencyProperty.Register("Employee", typeof(Employees_E), typeof(ShiftControl), new PropertyMetadata(Employees_E.VAKANT));
public Employees_E Employee
{
get { return (Employees_E)GetValue(EmployeeProperty); }
set { SetValue(EmployeeProperty, value); }
}...
I want the BaseColor to come from
Dictionary<Employees_E, int>
so the user can change either the Employee property or the value in the dictionary.

Under SelectionChanged read out the underlying data from a List

Im busy with my app and i walked in some problems when i click on a photo in my listbox PhotoFeed.
I got 1 List<> with in it the strings UrlTumb and UrlFull.
I got 1 ListBox with in it a WrapPanel filled with images wich i set the Image.Source from my UrlTumb.
What my problem is when i click on a photo in my listbox i want to navigate to a new page and display there the original image (UrlFull) now i can only get my UrlTumb from my Image.Source but i want my UrlFull which is stored in the List. Now is my question how do i obtain the UrlFull. So how can i back trace which item i clicked and get the UrlFull from that item so i can send it with my NavigationService.Navigate
I can do it on an dirty way and create an invisible textblock besides the image in my ListBox and put the UrlFull in there but i would like to do it in a proper way
So what do i place in the ????? spot in this line
NavigationService.Navigate(new Uri("/PhotoInfo.xaml?urlfull={0}", ????? , UriKind.Relative));
Greetings Cn
There are multiple options:
Use selected item's index listBox.SelectedIndex to get the index
of the selected property which will correspond to the index in your
source (it might not if you filter the collection using collection
source, but I think that is not the case)
Use selected item listBox.SelectedItem this will return the
SelectedItem which will contain your object. (Note, that if your
selection mode set to multiple, this will return only the firstly
selected item)
Use SelectemItems. It will allow you to get an array of selected
items (Note: this should be normally used only when your list's
selection mode is set to multiple)
Use SelectedValue, which will contain the value of the SelectedItem
(this will save you and extra step.
Use arguments of the Selection changed event AddedItems.
Bellow is the code snippet of 3 options above. x, y, z will all be your selected names (e.g. "Mike")
XAML:
<ListBox x:Name="lb"
ItemsSource="{Binding Names}"
SelectionChanged="NameChanged" />
Code behind:
public class Person
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}
private List<Person> people = new List<Person>
{
new Person{Name = "Lewis"},
new Person{Name = "Peter"},
new Person{Name = "Brian"}
};
public List<Person> People
{
get
{
return this.people;
}
set
{
this.people = value;
}
}
private void NameChanged(object sender, SelectionChangedEventArgs e)
{
var x = this.people[lb.SelectedIndex];
var y = lb.SelectedItem;
var z = lb.SelectedItems[0];
var h = lb.SelectedValue;
var u = e.AddedItems[0];
var person = e.AddedItems[0] as Person;
if (person != null)
{
var result = person.Name;
}
}
For the differences between SelectedValue and SelectedItem refer here SelectedItem vs SelectedValue

A collection of StackPanel as ItemsSource of ComboBox

I have
a collection of StackPanel which each one includes a dynamic set of controls (based on database values), I want to set them as ItemsSource of some ComboBox
for example i have two database values which should be generated:
In DB i have these:
row 1=>Class [a] p [B] , [AB]vb
row 2=>Class tpy rbs=[sdfg],kssc[h] hm
and each one should generate as a ComboBox column like the fallowing:
In ComboBox I wanna generate these :
ComboBoxItem 1 :Class [a textBox] p [a textBox] , [a textBox]vb
ComboBoxItem 2 :Class tpy rbs=[a textBox].kssc[a textBox] hm
the fallowing code is doing this right:
Class ConvertToControlsFormat()
{
Regex exp = new Regex(#"\[\w*\]");
var source = new TestEntities().cmbSources;
foreach (var item in source)
{
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(item.Title))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(item.Title).Count)
p.Children.Add(new TextBox { Text = exp.Matches(item.Title)[i].Value, Width = 30 });
}
cmb.Items.Add(p);
}
}
But I cant set TwoWay DataBindings for that, so I created a list of StackPanel as a field of cmbSource class (which is bound to ItemsSource of the ComboBox)
public partial class cmbSource
{
#region Primitive Properties
int iD;
public virtual int ID
{
get
{
if (Title != null)
ControlsCollection = SetControlsCollection(Title);
return iD;
}
set
{
iD = value;
}
}
private StackPanel SetControlsCollection(string ttl)
{
Regex exp = new Regex(#"\[\w*\]");
StackPanel p = new StackPanel { Orientation = Orientation.Horizontal, FlowDirection = System.Windows.FlowDirection.LeftToRight };
int i = 0;
foreach (string txt in exp.Split(ttl))
{
p.Children.Add(new TextBlock { Text = txt });
if (i < exp.Matches(ttl).Count)
p.Children.Add(new TextBox { Text = exp.Matches(ttl)[i].Value, Width = 30 });
}
return p;
}
public virtual string Title
{
get;
set;
}
public virtual StackPanel ControlsCollection
{
get;
set;
}
#endregion
}
but I have no idea of how bind it to ItemsSource of my ComboBox
Summery:I want to bind a list of controls to a ComboBox
any suggestions!? thank you.
EDIT
First: you do not bind a ComboBox to a collection of UI Elements. That is not the way WPF works. Container controls such as the Grid, StackPanel and Canvas can contain child controls. ItemsControls such as the ComboBox contain data objects and use DataTemplates to display the items.
Secondly: if the database can contain ANY data that could cause ANY UI to be needed you will need to generate the UI in code by creating StackPanels etc. adding controls and bindings as you do in your code examples.
Thirdly: the reason you can't bind is that the data from the database is a string that you split into parts; there is no way you can simply go back to the string.
Suggestion: the string in the database is probably (I hope) in some sort of format. Using that knowledge you could generate a new format string when you are parsing the database string. E.g., when the database contains foo [bar] you could generate {0} [bar]. On a save action from the user you could use that string to create the updated string for the database by using: String.Format("{0} [bar]", someControl.Text)
Extra: Please, next time, use better names and example texts; the question is unreadable like this. There is no way you can expect us to understand 2=>Class tpy rbs=[sdfg],kssc[h] hm
OLD ANSWER
Make a class Stuff, implementing INotifyPropertyChanged and having the properties Name and Value.
Load the database data into an ObservableCollection<Stuff> and bind the ComboBox to this collection.
Set the ItemTemplate of the combo box to a datatemplate like this:
<ComboBox ItemsSource="{Binding}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

WPF MVVM Light - Binding of SelectedItem not changing

Quite a few posts around this area, but none are helping me... here's the scenario: I've got two "season" drop downs to simulate a range. If you pick a season in the begin range one, the viewmodele automatically sets the property bound to the end range to the same season (so it defaults to a single year and not a range. Here's what the XAML looks like (removed lot of the formatting attibutes for readability):
<ComboBox ItemsSource="{Binding AvailableSeasons, Mode=OneWay}"
SelectedItem="{Binding SelectedBeginRangeSeason, Mode=TwoWay}"
ItemTemplate="{DynamicResource SeasonItemShortFormat}" />
<ComboBox ItemsSource="{Binding AvailableSeasons, Mode=OneWay}"
SelectedItem="{Binding SelectedEndRangeSeason, Mode=TwoWay}"
ItemTemplate="{DynamicResource SeasonItemShortFormat}" />
The properties in the view model look like this:
private Season _selectedBeginRangeSeason;
private const string SelectedBeginRangeSeasonPropertyName = "SelectedBeginRangeSeason";
public Season SelectedBeginRangeSeason {
get { return _selectedBeginRangeSeason; }
set {
if (_selectedBeginRangeSeason != value) {
var oldValue = _selectedBeginRangeSeason;
_selectedBeginRangeSeason = value;
RaisePropertyChanged<Season>(SelectedBeginRangeSeasonPropertyName, oldValue, value, true);
}
}
}
private Season _selectedEndRangeSeason;
private const string SelectedEndRangeSeasonPropertyName = "SelectedEndRangeSeason";
public Season SelectedEndRangeSeason {
get { return _selectedEndRangeSeason; }
set {
if (_selectedEndRangeSeason != value) {
Debug.WriteLine("Updating property SelectedEndRangeSeason...");
var oldValue = _selectedEndRangeSeason;
_selectedEndRangeSeason = value;
Debug.WriteLine("Broadcasting PropertyChanged event for property SelectedEndRangeSeason...");
RaisePropertyChanged<Season>(SelectedEndRangeSeasonPropertyName, oldValue, value, true);
}
}
}
private void UpdateSelectedSeasonSelectors() {
// if the end range isn't selected...
if (_selectedEndRangeSeason == null) {
// automatically select the begin for the end range
SelectedEndRangeSeason = _selectedBeginRangeSeason;
}
}
I've verified the end property is being changed both with the debug statements and with unit tests, but the UI isn't changing when I select it... can't figure out what's going on and have looked at this so many different ways...
Did you get the SelectedSeason from the AvailableSeasons collection? If not, did you implement anything special to compare Seasons?
For example, suppose you have
<ComboBox ItemsSource="{Binding AvailableSeasons}"
SelectedItem="{Binding SelectedSeason}" />
If SelectedSeason = new Season(); the SelectedItem binding won't work because new Season(); does not exist in AvailableSeasons.
You'll need to set SelectedSeason = AvailableSeasons[x] for SelectedItem to work because that makes the two items exactly the same. Or you can implement some custom method to compare the two seasons to see if they're the same. Usually I just overwrite the ToString() method of the class being compared.
Try to fire an event from the ViewModel to notify the UI to refresh the calendar.

wpf toolkit, datagrid, comboboxcolumn

In a datagrid I have two DataGridComboBoxColumns. The items of one of these columns should depend on what is selected in the other column. The underlying collection used to model this is a dictionary<string,List<string>>. How should i go about implementing this? I can't seem to hook up to any relevant events on the columns, and I cant find any databinding scenarios that support this..
I had the same scenario a while back and fixed it like this:
public class DataItem : INotifyPropertyChanged {
...
public List<SomeObject> DisplayableComboBoxItems {
get; set;
}
private static Dictionary<int, List<SomeObject>> myDict;
public Dictionary<int, List<SomeObject>> MyDict {
get {
if (myDict == null) {
myDict = GetYourDataFromSomewhere();
}
return myDict;
}
}
public int TypeId {
get { return typeId; }
set {
if (value == typeId) return;
typeId = value;
RaisePropertyChanged("TypeId");
}
}
public int TypeSetId {
get { return typeSetId; }
set {
if (typeSetId == value) return;
typeSetId = value;
RaisePropertyChanged("TypeSetId");
DisplayableComboBoxItems = MyDict[typeSetId];
RaisePropertyChanged("DisplayableComboBoxItems");
TypeId = 0;
}
}
...
}
DataItem is the object that gets bound to a DataRow.
This is just a small mock-up of the code. Basically, whenever the TypeSet changes, I needed a new list of Types to be displayed. I used just a static list, in this example i used a dictionary.
With this setup you can bind you combobox ItemsSource to the 'DisplayableComboBoxItems', and your SelectedValue to "TypeId".
You're gonna need other properties to display the correct text instead of the TypeId.
The downside of this is that when you have 1000+ items, you'll have that same list for all items. This wasn't however the case with me (DataGrid showed max 50 items).
I hope this is clear enough and that it helps you in the right direction!
cheers!
Roel
Instead of using a DataGridComboBoxColumn for the second column, I went with a DataGridTemplateColumn with an embedded Combobox. For the itemsource i defined a converter: string -> List<string>. The converter translates the value of the selecteditem of the other DataGridComboBox (which is bound to Navn) into List<string>, this is just a dictionary lookup.
Like so:
<my:DataGridTemplateColumn>
<my:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding Værdi}"
ItemsSource="{Binding Navn, Converter={StaticResource dimensionToValues}}"
>
</ComboBox>
</DataTemplate>
</my:DataGridTemplateColumn.CellTemplate>
</my:DataGridTemplateColumn>

Resources